-
Notifications
You must be signed in to change notification settings - Fork 3.4k
Cosmos db provider: Implement owned type null comparison #37321
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
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: dotnet#24087
249ae47 to
6b86657
Compare
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as resolved.
This comment was marked as resolved.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Implements correct null/not-null comparison translation for owned types in the Cosmos provider by using IS_NULL(...) OR NOT IS_DEFINED(...) semantics (and avoiding null checks on document roots), and updates/extends query tests accordingly.
Changes:
- Update Cosmos structural/entity equality translation to generate
IS_NULL/IS_DEFINEDchecks for null comparisons (and simplify always-true/always-false document-root comparisons). - Add new structural-equality test coverage for optional-nested-owned null/not-null comparisons across providers.
- Update Cosmos functional test baselines to reflect the new translation behavior.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs | Changes null equality translation to use IS_NULL/IS_DEFINED and avoids null checks on document roots. |
| src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs | Skips applying predicates that are effectively constant-true (NOT false) to avoid redundant filters. |
| test/EFCore.Specification.Tests/Query/Associations/AssociationsStructuralEqualityTestBase.cs | Adds base tests for optional-associate nested null/not-null scenarios. |
| test/EFCore.Cosmos.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsStructuralEqualityCosmosTest.cs | Updates Cosmos assertions to validate new null/not-null SQL translation for owned navigations. |
| test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs | Updates Cosmos baselines for entity null/not-null comparisons to match new constant folding. |
| test/EFCore.Sqlite.FunctionalTests/Query/Associations/OwnedTableSplitting/OwnedTableSplittingStructuralEqualitySqliteTest.cs | Adds provider-specific SQL baselines for the new optional-nested null/not-null tests. |
| test/EFCore.Sqlite.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsStructuralEqualitySqliteTest.cs | Adds provider-specific SQL baselines for the new optional-nested null/not-null tests. |
| test/EFCore.Sqlite.FunctionalTests/Query/Associations/OwnedJson/OwnedJsonStructuralEqualitySqliteTest.cs | Adds provider-specific SQL baselines for the new optional-nested null/not-null tests (JSON). |
| test/EFCore.SqlServer.FunctionalTests/Query/Associations/OwnedTableSplitting/OwnedTableSplittingStructuralEqualitySqlServerTest.cs | Adds provider-specific SQL baselines for the new optional-nested null/not-null tests. |
| test/EFCore.SqlServer.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsStructuralEqualitySqlServerTest.cs | Adds provider-specific SQL baselines for the new optional-nested null/not-null tests. |
| test/EFCore.SqlServer.FunctionalTests/Query/Associations/OwnedJson/OwnedJsonStructuralEqualitySqlServerTest.cs | Adds provider-specific SQL baselines for the new optional-nested null/not-null tests (JSON). |
| test/EFCore.SqlServer.FunctionalTests/Query/Associations/Navigations/NavigationsStructuralEqualitySqlServerTest.cs | Adds provider-specific SQL baselines for the new optional-nested null/not-null tests (non-owned navigations). |
| test/EFCore.SqlServer.FunctionalTests/Query/Associations/ComplexTableSplitting/ComplexTableSplittingStructuralEqualitySqlServerTest.cs | Adds provider-specific SQL baselines for the new optional-nested null/not-null tests (complex/table-splitting). |
| test/EFCore.SqlServer.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonStructuralEqualitySqlServerTest.cs | Adds provider-specific SQL baselines for the new optional-nested null/not-null tests (complex/JSON). |
| ? "!" + nameof(object.Equals) | ||
| : "!=", | ||
| entityType1.DisplayName())); | ||
| // Document root can never be be null |
Copilot
AI
Jan 29, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Comment typo: "Document root can never be be null" has a duplicated word; please correct to "can never be null".
| // Document root can never be be null | |
| // Document root can never be null |
| // 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))); | ||
| 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; | ||
| } |
Copilot
AI
Jan 29, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Entity equality/null comparison no longer throws for keyless entity types: the keyless check is skipped in the null-comparison branch and only enforced for non-null comparisons. Other providers still throw for entity == null when the entity type is keyless (e.g. RelationalSqlTranslatingExpressionVisitor.StructuralEquality.cs:146-157). If this isn’t intentionally Cosmos-specific, consider preserving the keyless exception behavior here as well to avoid inconsistent semantics across providers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought it was ok to not throw in this case as we are able to easily compare to null the same way we do with keyfull entities. @roji What do you think?
| 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 } }) |
Copilot
AI
Jan 29, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This if condition is very long and likely exceeds the repo line-length guideline; please wrap it across multiple lines (and consider extracting the special-case check into a small helper/local to improve readability).
| if (translation is not SqlConstantExpression { Value: true } && translation is not SqlUnaryExpression { OperatorType: ExpressionType.Not, Operand: SqlConstantExpression { Value: false } }) | |
| static bool IsTriviallyTruePredicate(SqlExpression sqlExpression) | |
| => sqlExpression is SqlConstantExpression { Value: true } | |
| || sqlExpression is SqlUnaryExpression | |
| { | |
| OperatorType: ExpressionType.Not, | |
| Operand: SqlConstantExpression { Value: false } | |
| }; | |
| if (!IsTriviallyTruePredicate(translation)) |
| AssertSql( | ||
| """ | ||
| SELECT VALUE c | ||
| FROM root c | ||
| WHERE (IS_NULL(c["OptionalAssociate"]) OR NOT(IS_DEFINED(c["OptionalAssociate"]))) | ||
| """); |
Copilot
AI
Jan 29, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new AssertSql raw string literals aren’t indented consistently with the rest of the file (e.g. AssertSql( followed by """ at a different indentation). With C# raw string indentation rules, this can accidentally introduce leading whitespace into the expected SQL and make baselines fragile. Please align these """ blocks with the existing pattern used earlier in the file.
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
IS_NULL(c["Prop"]) OR NOT IS_DEFINED(c["Prop"])instead. Don't compare document roots as those can't be null. Fixes: #24087