Don't transform Cosmos query to ReadItem when WithPartitionKey conflicts with predicate partition key#38439
Don't transform Cosmos query to ReadItem when WithPartitionKey conflicts with predicate partition key#38439Copilot wants to merge 8 commits into
Conversation
…icate partition key Fixes #38238 Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR fixes an incorrect Cosmos optimization where a query could be transformed into a ReadItem call even when WithPartitionKey(...) conflicts with partition-key comparisons in the predicate—causing the predicate partition-key to be silently ignored and returning the wrong document. The change prevents the ReadItem transform in that scenario so the query executes normally and conflicting values correctly yield zero results.
Changes:
- Update
CosmosReadItemAndPartitionKeysExtractorto skip theReadItemtransform when partition key values come fromWithPartitionKey()and the predicate also contains partition key comparison(s). - Add new functional tests for the conflicting
WithPartitionKey+(id && partitionKey)predicate case (including leaf/inheritance variants). - Update multiple Cosmos functional test SQL baselines to reflect the new (non-
ReadItem) query shape in same-value scenarios.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| src/EFCore.Cosmos/Query/Internal/CosmosReadItemAndPartitionKeysExtractor.cs | Adds a predicate-partition-key-comparison flag and blocks ReadItem transform when WithPartitionKey() may conflict with predicate PK comparisons. |
| test/EFCore.Cosmos.FunctionalTests/Query/ReadItemPartitionKeyQueryTestBase.cs | Adds a new base test covering conflicting WithPartitionKey + predicate partition-key comparison. |
| test/EFCore.Cosmos.FunctionalTests/Query/ReadItemPartitionKeyQueryInheritanceTestBase.cs | Adds a leaf/inheritance variant of the conflicting-case test. |
| test/EFCore.Cosmos.FunctionalTests/Query/ReadItemPartitionKeyQueryTest.cs | Updates baselines to expect regular query SQL (not ReadItem) and adds baseline for the new conflicting-case test. |
| test/EFCore.Cosmos.FunctionalTests/Query/ReadItemPartitionKeyQueryRootDiscriminatorInIdTest.cs | Updates baselines for non-ReadItem query shape and adds baselines for the new conflicting-case tests. |
| test/EFCore.Cosmos.FunctionalTests/Query/ReadItemPartitionKeyQueryNoDiscriminatorInIdTest.cs | Updates baselines for non-ReadItem query shape and adds baselines for the new conflicting-case tests. |
| test/EFCore.Cosmos.FunctionalTests/Query/ReadItemPartitionKeyQueryDiscriminatorInIdTest.cs | Updates baselines for non-ReadItem query shape and adds baselines for the new conflicting-case tests. |
Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
…ng LINQ + ApplyTypeMapping Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
…titionKey + same predicate When a literal is passed to WithPartitionKey() it gets funcletized to a SqlParameterExpression (outside a lambda), while the same literal in a Where() stays as a SqlConstantExpression. SqlExpression.Equals() returned false for different subtypes even with identical values, so the conflict detection incorrectly disabled ReadItem for same-value cases. Fix: when withPkValue is a parameter and predicateValue is a constant, and all partition key properties are encoded in the JSON ID, ReadItem is safe regardless of runtime value — a wrong partition key returns null rather than the wrong document. Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
| ss => ss.Set<SinglePartitionKeyEntity>().Where(e => e.PartitionKey == "PK1") | ||
| .Where(e => e.Id == Guid.Parse("B29BCED8-E1E5-420E-82D7-1C7A51703D34") && e.PartitionKey == "PK2"), | ||
| assertEmpty: true); | ||
|
|
There was a problem hiding this comment.
@copilot Also add
[Fact]
public virtual Task WithPartitionKey_and_predicate_with_id_two_locals_same_value()
{
var pkForWith = "PK1";
var pkForWhere = "PK1";
return AssertQuery(
async: true,
ss => ss.Set<SinglePartitionKeyEntity>().WithPartitionKey(pkForWith)
.Where(e => e.Id == Guid.Parse("B29BCED8-E1E5-420E-82D7-1C7A51703D34") && e.PartitionKey == pkForWhere),
ss => ss.Set<SinglePartitionKeyEntity>().Where(e => e.PartitionKey == pkForWith)
.Where(e => e.Id == Guid.Parse("B29BCED8-E1E5-420E-82D7-1C7A51703D34") && e.PartitionKey == pkForWhere));
}
There was a problem hiding this comment.
Added in the latest commit. The test is in the base class and overridden in all four concrete test files with SQL baselines.
Since pkForWith and pkForWhere are two different local variables (even though both hold "PK1"), they produce two different SqlParameterExpressions at translate time (@pkForWith and @pkForWhere). The conflict check compares by expression equality (which uses parameter names), so they are not recognized as equal → ReadItem is skipped and the query runs as a full SQL query with @pkForWhere in the WHERE clause. The SQL baselines reflect this behavior.
…oop, shorten comments Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
Combining
WithPartitionKey(...)with a predicate that also compares the partition key produced aReadItemusing theWithPartitionKeyvalue, silently discarding the predicate. A conflicting value returned theWithPartitionKeydocument instead of zero results.Changes
CosmosReadItemAndPartitionKeysExtractor: skip the ReadItem transform only when partition key values supplied viaWithPartitionKey()actually conflict with partition key comparisons in the predicate. Matching values remain eligible forReadItem, while conflicting values execute as a regular query so the predicate is preserved and yields zero results.ReadItemeligibility.predicateValue.TypeMappingto theWithPartitionKey()value before comparing (sinceWithPartitionKey()arguments are translated withapplyDefaultTypeMapping:false), ensuring identical constant/parameter pairs are correctly recognized as non-conflicting. A special case handles theSqlParameterExpressionvsSqlConstantExpressionmismatch that arises when a literal is passed toWithPartitionKey()(funcletized to a parameter) but written inline in the predicate (stays a constant): when all partition key properties are also part of the JSON ID, ReadItem is safe regardless.Behavior change
WithPartitionKey()and an equivalent predicate partition-key comparison (same values) continue to translate toReadItem.WithPartitionKey()and conflicting predicate partition-key comparisons no longer translate toReadItem; they execute as regular queries so the conflict is honored and no result is returned.WithPartitionKey(pkVar)andPartitionKey == differentPkVarwhere both are different local variables holding the same runtime value cannot be recognized as equal at translate time and fall through to a full query (the result is still correct).Tests
WithPartitionKey_and_predicate_with_id_and_conflicting_partition_key(+_leafvariant) covering the conflicting case (asserts empty).WithPartitionKey_and_predicate_with_id_two_locals_same_valuedocumenting the behavior whenWithPartitionKeyand the predicate use two different local variables that happen to hold the same value — these cannot be recognized as equal at translate time, so the query falls through to a full SQL query (result is still correct).