Skip to content

Commit d4cb397

Browse files
authored
Cosmos: translate top-level Any() to LIMIT 1 instead of EXISTS (#38297)
Fixes #33854
1 parent b423c88 commit d4cb397

3 files changed

Lines changed: 102 additions & 107 deletions

File tree

src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -428,12 +428,48 @@ private ShapedQueryExpression CreateShapedQueryExpression(SelectExpression selec
428428
subquery.ClearOrdering();
429429
}
430430

431-
var translation = _sqlExpressionFactory.Exists(subquery);
432-
var selectExpression = new SelectExpression(translation);
431+
// For subqueries, Cosmos supports EXISTS over correlated subqueries (e.g. within a WHERE clause).
432+
if (_subquery)
433+
{
434+
var translation = _sqlExpressionFactory.Exists(subquery);
435+
var selectExpression = new SelectExpression(translation);
436+
437+
return source.Update(
438+
selectExpression,
439+
Expression.Convert(
440+
new ProjectionBindingExpression(selectExpression, new ProjectionMember(), typeof(bool?)), typeof(bool)));
441+
}
442+
443+
// For top-level Any(), Cosmos doesn't support EXISTS over uncorrelated subqueries. Instead, we project a constant 'true'
444+
// with LIMIT 1:
445+
// SELECT VALUE true FROM root c WHERE <predicate> OFFSET 0 LIMIT 1
446+
// If at least one document matches, we get back 'true'; if no documents match, the result set is empty.
447+
// We override the result cardinality to SingleOrDefault so that an empty result returns default(bool) = false
448+
// (the caller sets ResultCardinality.Single which would throw on empty results).
449+
450+
// TODO: Subquery pushdown, #33968
451+
if (subquery.IsDistinct)
452+
{
453+
return null;
454+
}
455+
456+
subquery.ClearOrdering();
457+
458+
var topLevelTranslation = _sqlExpressionFactory.ApplyDefaultTypeMapping(_sqlExpressionFactory.Constant(true));
459+
var projectionMapping = new Dictionary<ProjectionMember, Expression> { { new ProjectionMember(), topLevelTranslation } };
460+
subquery.ReplaceProjectionMapping(projectionMapping);
461+
462+
if (!TryApplyLimit(subquery, TranslateExpression(Expression.Constant(1))!))
463+
{
464+
return null;
465+
}
433466

434-
return source.Update(
435-
selectExpression,
436-
Expression.Convert(new ProjectionBindingExpression(selectExpression, new ProjectionMember(), typeof(bool?)), typeof(bool)));
467+
return source
468+
.UpdateShaperExpression(
469+
Expression.Convert(
470+
new ProjectionBindingExpression(source.QueryExpression, new ProjectionMember(), typeof(bool?)),
471+
typeof(bool)))
472+
.UpdateResultCardinality(ResultCardinality.SingleOrDefault);
437473
}
438474

439475
/// <summary>

test/EFCore.Cosmos.FunctionalTests/Query/NorthwindAggregateOperatorsQueryCosmosTest.cs

Lines changed: 24 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2267,27 +2267,22 @@ WHERE NOT(false)
22672267
""");
22682268
});
22692269

2270-
public override async Task Contains_top_level(bool async)
2271-
{
2272-
// Always throws for sync.
2273-
if (async)
2274-
{
2275-
// Top-level Any(), see #33854.
2276-
var exception = await Assert.ThrowsAsync<CosmosException>(() => base.Contains_top_level(async));
2277-
2278-
Assert.Equal(HttpStatusCode.BadRequest, exception.StatusCode);
2270+
public override Task Contains_top_level(bool async)
2271+
=> Fixture.NoSyncTest(
2272+
async, async a =>
2273+
{
2274+
await base.Contains_top_level(a);
22792275

2280-
AssertSql(
2281-
"""
2276+
AssertSql(
2277+
"""
22822278
@p='ALFKI'
22832279
2284-
SELECT VALUE EXISTS (
2285-
SELECT 1
2286-
FROM root c
2287-
WHERE (c["id"] = @p))
2280+
SELECT VALUE true
2281+
FROM root c
2282+
WHERE (c["id"] = @p)
2283+
OFFSET 0 LIMIT 1
22882284
""");
2289-
}
2290-
}
2285+
});
22912286

22922287
public override async Task Contains_with_local_tuple_array_closure(bool async)
22932288
{
@@ -2519,28 +2514,22 @@ WHERE ARRAY_CONTAINS(@ids, c["id"])
25192514
""");
25202515
});
25212516

2522-
public override async Task Contains_over_entityType_with_null_should_rewrite_to_false(bool async)
2523-
{
2524-
// Always throws for sync.
2525-
if (async)
2526-
{
2527-
// Top-level Any(), see #33854.
2528-
var exception =
2529-
await Assert.ThrowsAsync<CosmosException>(() => base.Contains_over_entityType_with_null_should_rewrite_to_false(async));
2530-
2531-
Assert.Equal(HttpStatusCode.BadRequest, exception.StatusCode);
2517+
public override Task Contains_over_entityType_with_null_should_rewrite_to_false(bool async)
2518+
=> Fixture.NoSyncTest(
2519+
async, async a =>
2520+
{
2521+
await base.Contains_over_entityType_with_null_should_rewrite_to_false(a);
25322522

2533-
AssertSql(
2534-
"""
2523+
AssertSql(
2524+
"""
25352525
@entity_equality_p_OrderID=null
25362526
2537-
SELECT VALUE EXISTS (
2538-
SELECT 1
2539-
FROM root c
2540-
WHERE (((c["$type"] = "Order") AND (c["CustomerID"] = "VINET")) AND (c["OrderID"] = @entity_equality_p_OrderID)))
2527+
SELECT VALUE true
2528+
FROM root c
2529+
WHERE (((c["$type"] = "Order") AND (c["CustomerID"] = "VINET")) AND (c["OrderID"] = @entity_equality_p_OrderID))
2530+
OFFSET 0 LIMIT 1
25412531
""");
2542-
}
2543-
}
2532+
});
25442533

25452534
public override async Task Contains_over_entityType_with_null_in_projection(bool async)
25462535
{

test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs

Lines changed: 37 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -801,43 +801,34 @@ await AssertTranslationFailedWithDetails(
801801
AssertSql();
802802
}
803803

804-
public override async Task Any_simple(bool async)
805-
{
806-
// Always throws for sync.
807-
if (async)
808-
{
809-
// Top-level Any(), see #33854.
810-
var exception = await Assert.ThrowsAsync<CosmosException>(() => base.Any_simple(async));
811-
812-
Assert.Equal(HttpStatusCode.BadRequest, exception.StatusCode);
804+
public override Task Any_simple(bool async)
805+
=> Fixture.NoSyncTest(
806+
async, async a =>
807+
{
808+
await base.Any_simple(a);
813809

814-
AssertSql(
815-
"""
816-
SELECT VALUE EXISTS (
817-
SELECT 1
818-
FROM root c)
810+
AssertSql(
811+
"""
812+
SELECT VALUE true
813+
FROM root c
814+
OFFSET 0 LIMIT 1
819815
""");
820-
}
821-
}
816+
});
822817

823-
public override async Task Any_predicate(bool async)
824-
{
825-
// Always throws for sync.
826-
if (async)
827-
{
828-
// Top-level Any(), see #33854.
829-
var exception = await Assert.ThrowsAsync<CosmosException>(() => base.Any_predicate(async));
818+
public override Task Any_predicate(bool async)
819+
=> Fixture.NoSyncTest(
820+
async, async a =>
821+
{
822+
await base.Any_predicate(a);
830823

831-
Assert.Equal(HttpStatusCode.BadRequest, exception.StatusCode);
832-
AssertSql(
833-
"""
834-
SELECT VALUE EXISTS (
835-
SELECT 1
836-
FROM root c
837-
WHERE STARTSWITH(c["ContactName"], "A"))
824+
AssertSql(
825+
"""
826+
SELECT VALUE true
827+
FROM root c
828+
WHERE STARTSWITH(c["ContactName"], "A")
829+
OFFSET 0 LIMIT 1
838830
""");
839-
}
840-
}
831+
});
841832

842833
public override async Task Any_nested_negated(bool async)
843834
{
@@ -1295,26 +1286,10 @@ public override Task Skip_Take_Distinct(bool async)
12951286

12961287
public override async Task Skip_Take_Any(bool async)
12971288
{
1298-
// Always throws for sync.
1299-
if (async)
1300-
{
1301-
// Top-level Any(), see #33854.
1302-
var exception = await Assert.ThrowsAsync<CosmosException>(() => base.Skip_Take_Any(async));
1303-
1304-
Assert.Equal(HttpStatusCode.BadRequest, exception.StatusCode);
1305-
1306-
AssertSql(
1307-
"""
1308-
@p='5'
1309-
@p1='10'
1289+
// TODO: Subquery pushdown, #33968
1290+
await AssertTranslationFailed(() => base.Skip_Take_Any(async));
13101291

1311-
SELECT VALUE EXISTS (
1312-
SELECT 1
1313-
FROM root c
1314-
ORDER BY c["ContactName"]
1315-
OFFSET @p LIMIT @p1)
1316-
""");
1317-
}
1292+
AssertSql();
13181293
}
13191294

13201295
public override async Task Skip_Take_All(bool async)
@@ -1506,24 +1481,19 @@ WHERE STARTSWITH(c["id"], "A")
15061481
}
15071482
}
15081483

1509-
public override async Task OrderBy_ThenBy_Any(bool async)
1510-
{
1511-
// Always throws for sync.
1512-
if (async)
1513-
{
1514-
// Top-level Any(), see #33854.
1515-
var exception = await Assert.ThrowsAsync<CosmosException>(() => base.OrderBy_ThenBy_Any(async));
1516-
1517-
Assert.Equal(HttpStatusCode.BadRequest, exception.StatusCode);
1484+
public override Task OrderBy_ThenBy_Any(bool async)
1485+
=> Fixture.NoSyncTest(
1486+
async, async a =>
1487+
{
1488+
await base.OrderBy_ThenBy_Any(a);
15181489

1519-
AssertSql(
1520-
"""
1521-
SELECT VALUE EXISTS (
1522-
SELECT 1
1523-
FROM root c)
1490+
AssertSql(
1491+
"""
1492+
SELECT VALUE true
1493+
FROM root c
1494+
OFFSET 0 LIMIT 1
15241495
""");
1525-
}
1526-
}
1496+
});
15271497

15281498
public override async Task OrderBy_correlated_subquery1(bool async)
15291499
{

0 commit comments

Comments
 (0)