Skip to content

Conversation

@JoasE
Copy link
Contributor

@JoasE JoasE commented Dec 8, 2025

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

  • I've read the guidelines for contributing and seen the walkthrough
  • I've posted a comment on an issue with a detailed description of how I am planning to contribute and got approval from a member of the team
  • The code builds and tests pass locally (also verified by our automated build checks)
  • Commit messages follow this format:
        Summary of the changes
        - Detail 1
        - Detail 2

        Fixes #bugnumber
  • Tests for the changes have been added (for bug fixes / features)
  • Code follows the same patterns and style as existing code in this repo

JoasE added 4 commits December 8, 2025 15:07
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
@JoasE JoasE marked this pull request as ready for review December 8, 2025 19:25
@JoasE JoasE requested a review from a team as a code owner December 8, 2025 19:25
@roji roji force-pushed the main branch 2 times, most recently from 249ae47 to 6b86657 Compare January 13, 2026 17:46
@JoasE

This comment was marked as off-topic.

@JoasE

This comment was marked as resolved.

Copilot AI review requested due to automatic review settings January 29, 2026 10:15
Copy link

Copilot AI left a 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_DEFINED checks 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
Copy link

Copilot AI Jan 29, 2026

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".

Suggested change
// Document root can never be be null
// Document root can never be null

Copilot uses AI. Check for mistakes.
Comment on lines +1079 to 1101
// 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;
}
Copy link

Copilot AI Jan 29, 2026

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.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

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 } })
Copy link

Copilot AI Jan 29, 2026

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).

Suggested change
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))

Copilot uses AI. Check for mistakes.
Comment on lines +55 to +60
AssertSql(
"""
SELECT VALUE c
FROM root c
WHERE (IS_NULL(c["OptionalAssociate"]) OR NOT(IS_DEFINED(c["OptionalAssociate"])))
""");
Copy link

Copilot AI Jan 29, 2026

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.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Question on Cosmos DB database provider owned object querying null

3 participants