diff --git a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs index 29010cc544f..7c0e5934361 100644 --- a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs @@ -395,7 +395,24 @@ private static ShapedQueryExpression CreateShapedQueryExpression(IEntityType ent ShapedQueryExpression source, Expression index, bool returnDefault) - => null; + { + var selectExpression = (SelectExpression)source.QueryExpression; + var translation = TranslateExpression(index); + if (translation == null) + { + return null; + } + + if (selectExpression.Orderings.Count == 0) + { + _queryCompilationContext.Logger.RowLimitingOperationWithoutOrderByWarning(); + } + + selectExpression.ApplyOffset(translation); + selectExpression.ApplyLimit(TranslateExpression(Expression.Constant(1))!); + + return source; + } /// protected override ShapedQueryExpression? TranslateExcept(ShapedQueryExpression source1, ShapedQueryExpression source2) diff --git a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs index 64009e35060..76005334272 100644 --- a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs @@ -35,9 +35,9 @@ public class RelationalSqlTranslatingExpressionVisitor : ExpressionVisitor QueryableMethods.LastWithPredicate, QueryableMethods.LastWithoutPredicate, QueryableMethods.LastOrDefaultWithPredicate, - QueryableMethods.LastOrDefaultWithoutPredicate - //QueryableMethodProvider.ElementAtMethodInfo, - //QueryableMethodProvider.ElementAtOrDefaultMethodInfo + QueryableMethods.LastOrDefaultWithoutPredicate, + QueryableMethods.ElementAt, + QueryableMethods.ElementAtOrDefault }; private static readonly List PredicateAggregateMethodInfos = new() @@ -346,13 +346,29 @@ protected override Expression VisitBinary(BinaryExpression binaryExpression) && SingleResultMethodInfos.Contains(nonNullMethodCallExpression.Method.GetGenericMethodDefinition())) { var source = nonNullMethodCallExpression.Arguments[0]; - if (nonNullMethodCallExpression.Arguments.Count == 2) + var genericMethod = nonNullMethodCallExpression.Method.GetGenericMethodDefinition(); + if (genericMethod == QueryableMethods.FirstWithPredicate + || genericMethod == QueryableMethods.FirstOrDefaultWithPredicate + || genericMethod == QueryableMethods.SingleWithPredicate + || genericMethod == QueryableMethods.SingleOrDefaultWithPredicate + || genericMethod == QueryableMethods.LastWithPredicate + || genericMethod == QueryableMethods.LastOrDefaultWithPredicate) { source = Expression.Call( QueryableMethods.Where.MakeGenericMethod(source.Type.GetSequenceType()), source, nonNullMethodCallExpression.Arguments[1]); } + else if ((genericMethod == QueryableMethods.ElementAt || genericMethod == QueryableMethods.ElementAtOrDefault) + && (nonNullMethodCallExpression.Arguments[1] is not ConstantExpression constantIndex + || constantIndex.Value is not int constantInt + || constantInt != 0)) + { + source = Expression.Call( + QueryableMethods.Skip.MakeGenericMethod(source.Type.GetSequenceType()), + source, + nonNullMethodCallExpression.Arguments[1]); + } var translatedSubquery = _queryableMethodTranslatingExpressionVisitor.TranslateSubquery(source); if (translatedSubquery != null) diff --git a/src/EFCore/Extensions/EntityFrameworkQueryableExtensions.cs b/src/EFCore/Extensions/EntityFrameworkQueryableExtensions.cs index 0eb2ef4abd7..6a65cac5449 100644 --- a/src/EFCore/Extensions/EntityFrameworkQueryableExtensions.cs +++ b/src/EFCore/Extensions/EntityFrameworkQueryableExtensions.cs @@ -275,6 +275,87 @@ public static Task LongCountAsync( #endregion + #region ElementAt/ElementAtOrDefault + + /// + /// Asynchronously returns the element at a specified index in a sequence. + /// + /// + /// + /// Multiple active operations on the same context instance are not supported. Use to ensure + /// that any asynchronous operations have completed before calling another method on this context. + /// See Avoiding DbContext threading issues for more information and examples. + /// + /// + /// See Querying data with EF Core for more information and examples. + /// + /// + /// The type of the elements of . + /// An to return the element from. + /// The zero-based index of the element to retrieve. + /// A to observe while waiting for the task to complete. + /// + /// A task that represents the asynchronous operation. + /// The task result contains the element at a specified index in a sequence. + /// + /// + /// is . + /// + /// + /// + /// is less than zero. + /// + /// + /// If the is canceled. + public static Task ElementAtAsync( + this IQueryable source, + int index, + CancellationToken cancellationToken = default) + { + Check.NotNull(index, nameof(index)); + + return ExecuteAsync>( + QueryableMethods.ElementAt, source, Expression.Constant(index), cancellationToken); + } + + /// + /// Asynchronously returns the element at a specified index in a sequence, or a default value if the index is out of range. + /// + /// + /// + /// Multiple active operations on the same context instance are not supported. Use to ensure + /// that any asynchronous operations have completed before calling another method on this context. + /// See Avoiding DbContext threading issues for more information and examples. + /// + /// + /// See Querying data with EF Core for more information and examples. + /// + /// + /// The type of the elements of . + /// An to return the element from. + /// The zero-based index of the element to retrieve. + /// A to observe while waiting for the task to complete. + /// + /// A task that represents the asynchronous operation. + /// The task result contains the element at a specified index in a sequence. + /// + /// + /// is . + /// + /// If the is canceled. + public static Task ElementAtOrDefaultAsync( + this IQueryable source, + int index, + CancellationToken cancellationToken = default) + { + Check.NotNull(index, nameof(index)); + + return ExecuteAsync>( + QueryableMethods.ElementAtOrDefault, source, Expression.Constant(index), cancellationToken); + } + + #endregion + #region First/FirstOrDefault /// diff --git a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.ExpressionVisitors.cs b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.ExpressionVisitors.cs index 221d56ebb04..a09f19db5d3 100644 --- a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.ExpressionVisitors.cs +++ b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.ExpressionVisitors.cs @@ -1000,10 +1000,13 @@ private sealed class ReducingExpressionVisitor : ExpressionVisitor if (navigationExpansionExpression.CardinalityReducingGenericMethodInfo != null) { + var arguments = new List { result }; + arguments.AddRange(navigationExpansionExpression.CardinalityReducingMethodArguments.Select(x => Visit(x))); + result = Expression.Call( navigationExpansionExpression.CardinalityReducingGenericMethodInfo.MakeGenericMethod( result.Type.GetSequenceType()), - result); + arguments.ToArray()); } return result; diff --git a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.Expressions.cs b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.Expressions.cs index 5aff0d78046..69d8229eb31 100644 --- a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.Expressions.cs +++ b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.Expressions.cs @@ -246,6 +246,7 @@ private set public Expression PendingSelector { get; private set; } public MethodInfo? CardinalityReducingGenericMethodInfo { get; private set; } + public List CardinalityReducingMethodArguments { get; private set; } = new(); public Type SourceElementType => CurrentParameter.Type; @@ -274,8 +275,11 @@ public void AppendPendingOrdering(MethodInfo orderingMethod, Expression keySelec public void ClearPendingOrderings() => _pendingOrderings.Clear(); - public void ConvertToSingleResult(MethodInfo genericMethod) - => CardinalityReducingGenericMethodInfo = genericMethod; + public void ConvertToSingleResult(MethodInfo genericMethod, params Expression[] arguments) + { + CardinalityReducingGenericMethodInfo = genericMethod; + CardinalityReducingMethodArguments.AddRange(arguments); + } public override ExpressionType NodeType => ExpressionType.Extension; diff --git a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs index 5f5b87ec477..22d87b508ee 100644 --- a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs +++ b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs @@ -433,6 +433,16 @@ when QueryableMethods.IsSumWithSelector(method): methodCallExpression.Arguments[1].UnwrapLambdaFromQuote(), methodCallExpression.Type); + case nameof(Queryable.ElementAt) + when genericMethod == QueryableMethods.ElementAt: + case nameof(Queryable.ElementAtOrDefault) + when genericMethod == QueryableMethods.ElementAtOrDefault: + return ProcessElementAt( + source, + genericMethod, + methodCallExpression.Arguments[1], + methodCallExpression.Type); + case nameof(Queryable.Join) when genericMethod == QueryableMethods.Join: { @@ -982,6 +992,22 @@ private NavigationExpansionExpression ProcessFirstSingleLastOrDefault( return source; } + private NavigationExpansionExpression ProcessElementAt( + NavigationExpansionExpression source, + MethodInfo genericMethod, + Expression index, + Type returnType) + { + if (source.PendingSelector.Type != returnType) + { + source.ApplySelector(Expression.Convert(source.PendingSelector, returnType)); + } + + source.ConvertToSingleResult(genericMethod, index); + + return source; + } + // This returns Expression since it can also return a deferred GroupBy operation private Expression ProcessGroupBy( NavigationExpansionExpression source, diff --git a/src/EFCore/Query/Internal/SubqueryMemberPushdownExpressionVisitor.cs b/src/EFCore/Query/Internal/SubqueryMemberPushdownExpressionVisitor.cs index fd04ec11ded..0f3c92d67c6 100644 --- a/src/EFCore/Query/Internal/SubqueryMemberPushdownExpressionVisitor.cs +++ b/src/EFCore/Query/Internal/SubqueryMemberPushdownExpressionVisitor.cs @@ -24,9 +24,9 @@ public class SubqueryMemberPushdownExpressionVisitor : ExpressionVisitor QueryableMethods.LastWithPredicate, QueryableMethods.LastWithoutPredicate, QueryableMethods.LastOrDefaultWithPredicate, - QueryableMethods.LastOrDefaultWithoutPredicate - //QueryableMethodProvider.ElementAtMethodInfo, - //QueryableMethodProvider.ElementAtOrDefaultMethodInfo + QueryableMethods.LastOrDefaultWithoutPredicate, + QueryableMethods.ElementAt, + QueryableMethods.ElementAtOrDefault }; private static readonly IDictionary PredicateLessMethodInfo = new Dictionary @@ -163,7 +163,13 @@ private Expression PushdownMember( var source = methodCallExpression.Arguments[0]; var queryableType = source.Type.GetSequenceType(); var genericMethod = methodCallExpression.Method.GetGenericMethodDefinition(); - if (methodCallExpression.Arguments.Count == 2) + + if (genericMethod == QueryableMethods.FirstWithPredicate + || genericMethod == QueryableMethods.FirstOrDefaultWithPredicate + || genericMethod == QueryableMethods.SingleWithPredicate + || genericMethod == QueryableMethods.SingleOrDefaultWithPredicate + || genericMethod == QueryableMethods.LastWithPredicate + || genericMethod == QueryableMethods.LastOrDefaultWithPredicate) { // Move predicate to Where so that we can change shape before operator source = Expression.Call( @@ -203,7 +209,16 @@ private Expression PushdownMember( Expression.Quote(Expression.Lambda(memberAccessExpression, parameter))); } - source = Expression.Call(genericMethod.MakeGenericMethod(source.Type.GetSequenceType()), source); + if (genericMethod == QueryableMethods.ElementAt + || genericMethod == QueryableMethods.ElementAtOrDefault) + { + var index = Visit(methodCallExpression.Arguments[1]); + source = Expression.Call(genericMethod.MakeGenericMethod(source.Type.GetSequenceType()), source, index); + } + else + { + source = Expression.Call(genericMethod.MakeGenericMethod(source.Type.GetSequenceType()), source); + } return source.Type != returnType ? Expression.Convert(source, returnType) diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs index 97c83867b6b..28602b874fb 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs @@ -4584,6 +4584,27 @@ public override async Task Select_subquery_recursive_trivial_returning_queryable AssertSql(); } + public override async Task Collection_navigation_equal_to_null_for_subquery_using_ElementAtOrDefault_constant_zero(bool async) + { + await AssertTranslationFailed(() => base.Collection_navigation_equal_to_null_for_subquery_using_ElementAtOrDefault_constant_zero(async)); + + AssertSql(); + } + + public override async Task Collection_navigation_equal_to_null_for_subquery_using_ElementAtOrDefault_constant_one(bool async) + { + await AssertTranslationFailed(() => base.Collection_navigation_equal_to_null_for_subquery_using_ElementAtOrDefault_constant_one(async)); + + AssertSql(); + } + + public override async Task Collection_navigation_equal_to_null_for_subquery_using_ElementAtOrDefault_parameter(bool async) + { + await AssertTranslationFailed(() => base.Collection_navigation_equal_to_null_for_subquery_using_ElementAtOrDefault_parameter(async)); + + AssertSql(); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindWhereQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindWhereQueryCosmosTest.cs index cdf0e2f8070..cea41b36843 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindWhereQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindWhereQueryCosmosTest.cs @@ -2413,6 +2413,22 @@ public override async Task First_over_custom_projection_compared_to_not_null(boo AssertSql(); } + public override async Task ElementAt_over_custom_projection_compared_to_not_null(bool async) + { + // Cosmos client evaluation. Issue #17246. + await AssertTranslationFailed(() => base.ElementAt_over_custom_projection_compared_to_not_null(async)); + + AssertSql(); + } + + public override async Task ElementAtOrDefault_over_custom_projection_compared_to_null(bool async) + { + // Cosmos client evaluation. Issue #17246. + await AssertTranslationFailed(() => base.ElementAtOrDefault_over_custom_projection_compared_to_null(async)); + + AssertSql(); + } + public override async Task Single_over_custom_projection_compared_to_null(bool async) { // Cosmos client evaluation. Issue #17246. diff --git a/test/EFCore.InMemory.FunctionalTests/Query/GearsOfWarQueryInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/Query/GearsOfWarQueryInMemoryTest.cs index 049f36e53e2..ce9292645ed 100644 --- a/test/EFCore.InMemory.FunctionalTests/Query/GearsOfWarQueryInMemoryTest.cs +++ b/test/EFCore.InMemory.FunctionalTests/Query/GearsOfWarQueryInMemoryTest.cs @@ -94,4 +94,19 @@ public override Task Null_semantics_is_correctly_applied_for_function_comparison // Null protection. Issue #13721. => Assert.ThrowsAsync( () => base.Null_semantics_is_correctly_applied_for_function_comparisons_that_take_arguments_from_optional_navigation(async)); + + public override Task ElementAt_basic_with_OrderBy(bool async) + => Task.CompletedTask; + + public override Task ElementAtOrDefault_basic_with_OrderBy(bool async) + => Task.CompletedTask; + + public override Task ElementAtOrDefault_basic_with_OrderBy_parameter(bool async) + => Task.CompletedTask; + + public override Task Where_subquery_with_ElementAtOrDefault_equality_to_null_with_composite_key(bool async) + => Task.CompletedTask; + + public override Task Where_subquery_with_ElementAt_using_column_as_index(bool async) + => Task.CompletedTask; } diff --git a/test/EFCore.InMemory.FunctionalTests/Query/NorthwindMiscellaneousQueryInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/Query/NorthwindMiscellaneousQueryInMemoryTest.cs index 348d88fd223..80923726e61 100644 --- a/test/EFCore.InMemory.FunctionalTests/Query/NorthwindMiscellaneousQueryInMemoryTest.cs +++ b/test/EFCore.InMemory.FunctionalTests/Query/NorthwindMiscellaneousQueryInMemoryTest.cs @@ -56,4 +56,13 @@ public override async Task Entity_equality_through_subquery_composite_key(bool a CoreStrings.EntityEqualityOnCompositeKeyEntitySubqueryNotSupported("==", nameof(OrderDetail)), (await Assert.ThrowsAsync( () => base.Entity_equality_through_subquery_composite_key(async))).Message); + + public override Task Collection_navigation_equal_to_null_for_subquery_using_ElementAtOrDefault_constant_zero(bool async) + => Task.CompletedTask; + + public override Task Collection_navigation_equal_to_null_for_subquery_using_ElementAtOrDefault_constant_one(bool async) + => Task.CompletedTask; + + public override Task Collection_navigation_equal_to_null_for_subquery_using_ElementAtOrDefault_parameter(bool async) + => Task.CompletedTask; } diff --git a/test/EFCore.InMemory.FunctionalTests/Query/NorthwindWhereQueryInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/Query/NorthwindWhereQueryInMemoryTest.cs index 34689d61840..8bd9ff38ab5 100644 --- a/test/EFCore.InMemory.FunctionalTests/Query/NorthwindWhereQueryInMemoryTest.cs +++ b/test/EFCore.InMemory.FunctionalTests/Query/NorthwindWhereQueryInMemoryTest.cs @@ -29,4 +29,10 @@ public override async Task Where_simple_closure(bool async) public override Task Like_with_non_string_column_using_double_cast(bool async) // Casting int to object to string is invalid for InMemory => Assert.ThrowsAsync(() => base.Like_with_non_string_column_using_double_cast(async)); + + public override Task ElementAt_over_custom_projection_compared_to_not_null(bool async) + => Task.CompletedTask; + + public override Task ElementAtOrDefault_over_custom_projection_compared_to_null(bool async) + => Task.CompletedTask; } diff --git a/test/EFCore.Relational.Specification.Tests/Query/ComplexNavigationsCollectionsSplitQueryRelationalTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/ComplexNavigationsCollectionsSplitQueryRelationalTestBase.cs index c6bc0acfe1e..cc99ad178e8 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/ComplexNavigationsCollectionsSplitQueryRelationalTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/ComplexNavigationsCollectionsSplitQueryRelationalTestBase.cs @@ -13,7 +13,11 @@ protected ComplexNavigationsCollectionsSplitQueryRelationalTestBase(TFixture fix } protected override Expression RewriteServerQueryExpression(Expression serverQueryExpression) - => new SplitQueryRewritingExpressionVisitor().Visit(serverQueryExpression); + { + serverQueryExpression = base.RewriteServerQueryExpression(serverQueryExpression); + + return new SplitQueryRewritingExpressionVisitor().Visit(serverQueryExpression); + } private class SplitQueryRewritingExpressionVisitor : ExpressionVisitor { diff --git a/test/EFCore.Relational.Specification.Tests/Query/ComplexNavigationsCollectionsSplitSharedTypeQueryRelationalTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/ComplexNavigationsCollectionsSplitSharedTypeQueryRelationalTestBase.cs index 2c52c8a19df..8c83897aba1 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/ComplexNavigationsCollectionsSplitSharedTypeQueryRelationalTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/ComplexNavigationsCollectionsSplitSharedTypeQueryRelationalTestBase.cs @@ -20,7 +20,11 @@ public override async Task SelectMany_with_navigation_and_Distinct_projecting_co () => base.SelectMany_with_navigation_and_Distinct_projecting_columns_including_join_key(async))).Message); protected override Expression RewriteServerQueryExpression(Expression serverQueryExpression) - => new SplitQueryRewritingExpressionVisitor().Visit(serverQueryExpression); + { + serverQueryExpression = base.RewriteServerQueryExpression(serverQueryExpression); + + return new SplitQueryRewritingExpressionVisitor().Visit(serverQueryExpression); + } private class SplitQueryRewritingExpressionVisitor : ExpressionVisitor { diff --git a/test/EFCore.Relational.Specification.Tests/Query/CompositeKeysSplitQueryRelationalTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/CompositeKeysSplitQueryRelationalTestBase.cs index dfb209aa3aa..3ccbba45a7d 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/CompositeKeysSplitQueryRelationalTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/CompositeKeysSplitQueryRelationalTestBase.cs @@ -12,7 +12,11 @@ public CompositeKeysSplitQueryRelationalTestBase(TFixture fixture) } protected override Expression RewriteServerQueryExpression(Expression serverQueryExpression) - => new SplitQueryRewritingExpressionVisitor().Visit(serverQueryExpression); + { + serverQueryExpression = base.RewriteServerQueryExpression(serverQueryExpression); + + return new SplitQueryRewritingExpressionVisitor().Visit(serverQueryExpression); + } private class SplitQueryRewritingExpressionVisitor : ExpressionVisitor { diff --git a/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs index 44c10730303..adb2a61365c 100644 --- a/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs @@ -8181,6 +8181,49 @@ public virtual Task Where_subquery_equality_to_null_without_composite_key(bool a async, ss => ss.Set().Where(s => s.Weapons.OrderBy(e => e.Name).FirstOrDefault() == null)); + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task ElementAt_basic_with_OrderBy(bool async) + => AssertElementAt( + async, + ss => ss.Set().OrderBy(g => g.FullName), + () => 0); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task ElementAtOrDefault_basic_with_OrderBy(bool async) + => AssertElementAtOrDefault( + async, + ss => ss.Set().OrderBy(g => g.FullName), + () => 1); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task ElementAtOrDefault_basic_with_OrderBy_parameter(bool async) + { + var prm = 2; + + return AssertElementAtOrDefault( + async, + ss => ss.Set().OrderBy(g => g.FullName), + () => prm); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Where_subquery_with_ElementAtOrDefault_equality_to_null_with_composite_key(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(s => s.Members.OrderBy(e => e.Nickname).ElementAtOrDefault(2) == null)); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Where_subquery_with_ElementAt_using_column_as_index(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(s => s.Members.OrderBy(m => m.Nickname).ElementAt(s.Id).Nickname == "Cole Train"), + ss => ss.Set().Where(s => s.Members.OrderBy(m => m.Nickname).ElementAtOrDefault(s.Id).Nickname == "Cole Train")); + protected GearsOfWarContext CreateContext() => Fixture.CreateContext(); diff --git a/test/EFCore.Specification.Tests/Query/NorthwindMiscellaneousQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindMiscellaneousQueryTestBase.cs index dc6b8326cf7..81bf6547724 100644 --- a/test/EFCore.Specification.Tests/Query/NorthwindMiscellaneousQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/NorthwindMiscellaneousQueryTestBase.cs @@ -5781,4 +5781,36 @@ public virtual Task Collection_projection_after_DefaultIfEmpty(bool async) assertOrder: true, elementAsserter: (e, a) => AssertCollection(e.Orders, a.Orders), entryCount: 14); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Collection_navigation_equal_to_null_for_subquery_using_ElementAtOrDefault_constant_zero(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(c => c.Orders.OrderBy(o => o.OrderID).ElementAtOrDefault(0).OrderDetails == null), + ss => ss.Set().Where(c => c.Orders.OrderBy(o => o.OrderID).ElementAtOrDefault(0) == null), + entryCount: 2); + + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Collection_navigation_equal_to_null_for_subquery_using_ElementAtOrDefault_constant_one(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(c => c.Orders.OrderBy(o => o.OrderID).ElementAtOrDefault(1).OrderDetails == null), + ss => ss.Set().Where(c => c.Orders.OrderBy(o => o.OrderID).ElementAtOrDefault(1) == null), + entryCount: 3); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Collection_navigation_equal_to_null_for_subquery_using_ElementAtOrDefault_parameter(bool async) + { + var prm = 2; + + return AssertQuery( + async, + ss => ss.Set().Where(c => c.Orders.OrderBy(o => o.OrderID).ElementAtOrDefault(prm).OrderDetails == null), + ss => ss.Set().Where(c => c.Orders.OrderBy(o => o.OrderID).ElementAtOrDefault(prm) == null), + entryCount: 5); + } } diff --git a/test/EFCore.Specification.Tests/Query/NorthwindWhereQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindWhereQueryTestBase.cs index 0ab7ee3cc27..1c2c568ccce 100644 --- a/test/EFCore.Specification.Tests/Query/NorthwindWhereQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/NorthwindWhereQueryTestBase.cs @@ -2326,6 +2326,24 @@ public virtual Task First_over_custom_projection_compared_to_not_null(bool async ss => ss.Set().Where(c => c.Orders.Select(o => new { o.OrderID }).FirstOrDefault() != null), entryCount: 89); + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task ElementAt_over_custom_projection_compared_to_not_null(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(c => c.Orders.Select(o => new { o.OrderID }).ElementAt(3) != null), + ss => ss.Set().Where(c => c.Orders.Select(o => new { o.OrderID }).ElementAtOrDefault(3) != null), + entryCount: 79); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task ElementAtOrDefault_over_custom_projection_compared_to_null(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(c => c.Orders.Select(o => new { o.OrderID }).ElementAtOrDefault(7) == null), + ss => ss.Set().Where(c => c.Orders.Select(o => new { o.OrderID }).ElementAtOrDefault(7) == null), + entryCount: 43); + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Single_over_custom_projection_compared_to_null(bool async) diff --git a/test/EFCore.Specification.Tests/Query/QueryTestBase.cs b/test/EFCore.Specification.Tests/Query/QueryTestBase.cs index fa14b3dccd4..45fc352f56a 100644 --- a/test/EFCore.Specification.Tests/Query/QueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/QueryTestBase.cs @@ -155,6 +155,44 @@ protected Task AssertAll( => QueryAsserter.AssertAll( actualQuery, expectedQuery, actualPredicate, expectedPredicate, async); + protected Task AssertElementAt( + bool async, + Func> query, + Func index, + Action asserter = null, + int entryCount = 0) + => AssertElementAt(async, query, query, index, index, asserter, entryCount); + + protected Task AssertElementAt( + bool async, + Func> actualQuery, + Func> expectedQuery, + Func actualIndex, + Func expectedIndex, + Action asserter = null, + int entryCount = 0) + => QueryAsserter.AssertElementAt( + actualQuery, expectedQuery, actualIndex, expectedIndex, asserter, entryCount, async); + + protected Task AssertElementAtOrDefault( + bool async, + Func> query, + Func index, + Action asserter = null, + int entryCount = 0) + => AssertElementAtOrDefault(async, query, query, index, index, asserter, entryCount); + + protected Task AssertElementAtOrDefault( + bool async, + Func> actualQuery, + Func> expectedQuery, + Func actualIndex, + Func expectedIndex, + Action asserter = null, + int entryCount = 0) + => QueryAsserter.AssertElementAtOrDefault( + actualQuery, expectedQuery, actualIndex, expectedIndex, asserter, entryCount, async); + protected Task AssertFirst( bool async, Func> query, diff --git a/test/EFCore.Specification.Tests/TestUtilities/QueryAsserter.cs b/test/EFCore.Specification.Tests/TestUtilities/QueryAsserter.cs index 6db110e96b2..12ac2a45e75 100644 --- a/test/EFCore.Specification.Tests/TestUtilities/QueryAsserter.cs +++ b/test/EFCore.Specification.Tests/TestUtilities/QueryAsserter.cs @@ -286,6 +286,50 @@ public async Task AssertAll( Assert.Equal(expected, actual); } + public async Task AssertElementAt( + Func> actualQuery, + Func> expectedQuery, + Func actualIndex, + Func expectedIndex, + Action asserter = null, + int entryCount = 0, + bool async = false, + bool filteredQuery = false) + { + using var context = _contextCreator(); + var actual = async + ? await RewriteServerQuery(actualQuery(SetSourceCreator(context))).ElementAtAsync(actualIndex()) + : RewriteServerQuery(actualQuery(SetSourceCreator(context))).ElementAt(actualIndex()); + + var expectedData = GetExpectedData(context, filteredQuery); + var expected = RewriteExpectedQuery(expectedQuery(expectedData)).ElementAt(expectedIndex()); + + AssertEqual(expected, actual, asserter); + AssertEntryCount(context, entryCount); + } + + public async Task AssertElementAtOrDefault( + Func> actualQuery, + Func> expectedQuery, + Func actualIndex, + Func expectedIndex, + Action asserter = null, + int entryCount = 0, + bool async = false, + bool filteredQuery = false) + { + using var context = _contextCreator(); + var actual = async + ? await RewriteServerQuery(actualQuery(SetSourceCreator(context))).ElementAtOrDefaultAsync(actualIndex()) + : RewriteServerQuery(actualQuery(SetSourceCreator(context))).ElementAtOrDefault(actualIndex()); + + var expectedData = GetExpectedData(context, filteredQuery); + var expected = RewriteExpectedQuery(expectedQuery(expectedData)).ElementAtOrDefault(expectedIndex()); + + AssertEqual(expected, actual, asserter); + AssertEntryCount(context, entryCount); + } + public async Task AssertFirst( Func> actualQuery, Func> expectedQuery, diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs index f520f973e7d..025617d3a8a 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs @@ -9952,6 +9952,86 @@ FROM [Gears] AS [g] """); } + public override async Task ElementAt_basic_with_OrderBy(bool async) + { + await base.ElementAt_basic_with_OrderBy(async); + + AssertSql( +""" +@__p_0='0' + +SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank] +FROM [Gears] AS [g] +ORDER BY [g].[FullName] +OFFSET @__p_0 ROWS FETCH NEXT 1 ROWS ONLY +"""); + } + + public override async Task ElementAtOrDefault_basic_with_OrderBy(bool async) + { + await base.ElementAtOrDefault_basic_with_OrderBy(async); + + AssertSql( +""" +@__p_0='1' + +SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank] +FROM [Gears] AS [g] +ORDER BY [g].[FullName] +OFFSET @__p_0 ROWS FETCH NEXT 1 ROWS ONLY +"""); + } + + public override async Task ElementAtOrDefault_basic_with_OrderBy_parameter(bool async) + { + await base.ElementAtOrDefault_basic_with_OrderBy_parameter(async); + + AssertSql( +""" +@__p_0='2' + +SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank] +FROM [Gears] AS [g] +ORDER BY [g].[FullName] +OFFSET @__p_0 ROWS FETCH NEXT 1 ROWS ONLY +"""); + } + + + public override async Task Where_subquery_with_ElementAtOrDefault_equality_to_null_with_composite_key(bool async) + { + await base.Where_subquery_with_ElementAtOrDefault_equality_to_null_with_composite_key(async); + + AssertSql( +""" +SELECT [s].[Id], [s].[Banner], [s].[Banner5], [s].[InternalNumber], [s].[Name] +FROM [Squads] AS [s] +WHERE NOT (EXISTS ( + SELECT 1 + FROM [Gears] AS [g] + WHERE [s].[Id] = [g].[SquadId] + ORDER BY [g].[Nickname] + OFFSET 2 ROWS)) +"""); + } + + public override async Task Where_subquery_with_ElementAt_using_column_as_index(bool async) + { + await base.Where_subquery_with_ElementAt_using_column_as_index(async); + + AssertSql( +""" +SELECT [s].[Id], [s].[Banner], [s].[Banner5], [s].[InternalNumber], [s].[Name] +FROM [Squads] AS [s] +WHERE ( + SELECT [g].[Nickname] + FROM [Gears] AS [g] + WHERE [s].[Id] = [g].[SquadId] + ORDER BY [g].[Nickname] + OFFSET [s].[Id] ROWS FETCH NEXT 1 ROWS ONLY) = N'Cole Train' +"""); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs index cec5b7b9278..e271f27eb7e 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs @@ -7174,6 +7174,57 @@ public override async Task EF_Property_include_on_incorrect_property_throws(bool AssertSql(); } + public override async Task Collection_navigation_equal_to_null_for_subquery_using_ElementAtOrDefault_constant_zero(bool async) + { + await base.Collection_navigation_equal_to_null_for_subquery_using_ElementAtOrDefault_constant_zero(async); + + AssertSql( +""" +SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] +FROM [Customers] AS [c] +WHERE NOT (EXISTS ( + SELECT 1 + FROM [Orders] AS [o] + WHERE [c].[CustomerID] = [o].[CustomerID])) +"""); + } + + public override async Task Collection_navigation_equal_to_null_for_subquery_using_ElementAtOrDefault_constant_one(bool async) + { + await base.Collection_navigation_equal_to_null_for_subquery_using_ElementAtOrDefault_constant_one(async); + + AssertSql( +""" +SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] +FROM [Customers] AS [c] +WHERE NOT (EXISTS ( + SELECT 1 + FROM [Orders] AS [o] + WHERE [c].[CustomerID] = [o].[CustomerID] + ORDER BY [o].[OrderID] + OFFSET 1 ROWS)) +"""); + } + + public override async Task Collection_navigation_equal_to_null_for_subquery_using_ElementAtOrDefault_parameter(bool async) + { + await base.Collection_navigation_equal_to_null_for_subquery_using_ElementAtOrDefault_parameter(async); + + AssertSql( +""" +@__prm_0='2' + +SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] +FROM [Customers] AS [c] +WHERE NOT (EXISTS ( + SELECT 1 + FROM [Orders] AS [o] + WHERE [c].[CustomerID] = [o].[CustomerID] + ORDER BY [o].[OrderID] + OFFSET @__prm_0 ROWS)) +"""); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs index 8482cf83ef2..e2aeed1450f 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs @@ -2799,6 +2799,40 @@ FROM [Orders] AS [o] """); } + public override async Task ElementAt_over_custom_projection_compared_to_not_null(bool async) + { + await base.ElementAt_over_custom_projection_compared_to_not_null(async); + + AssertSql( +""" +SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] +FROM [Customers] AS [c] +WHERE EXISTS ( + SELECT 1 + FROM [Orders] AS [o] + WHERE [c].[CustomerID] = [o].[CustomerID] + ORDER BY (SELECT 1) + OFFSET 3 ROWS) +"""); + } + + public override async Task ElementAtOrDefault_over_custom_projection_compared_to_null(bool async) + { + await base.ElementAtOrDefault_over_custom_projection_compared_to_null(async); + + AssertSql( +""" +SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] +FROM [Customers] AS [c] +WHERE NOT (EXISTS ( + SELECT 1 + FROM [Orders] AS [o] + WHERE [c].[CustomerID] = [o].[CustomerID] + ORDER BY (SELECT 1) + OFFSET 7 ROWS)) +"""); + } + public override async Task Single_over_custom_projection_compared_to_null(bool async) { await base.Single_over_custom_projection_compared_to_null(async); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs index a809b6c2ab1..48ffd729bb1 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs @@ -13017,6 +13017,115 @@ FROM [Officers] AS [o0] """); } + public override async Task ElementAt_basic_with_OrderBy(bool async) + { + await base.ElementAt_basic_with_OrderBy(async); + + AssertSql( +""" +@__p_0='0' + +SELECT [t].[Nickname], [t].[SquadId], [t].[AssignedCityName], [t].[CityOfBirthName], [t].[FullName], [t].[HasSoulPatch], [t].[LeaderNickname], [t].[LeaderSquadId], [t].[Rank], [t].[Discriminator] +FROM ( + SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank], N'Gear' AS [Discriminator] + FROM [Gears] AS [g] + UNION ALL + SELECT [o].[Nickname], [o].[SquadId], [o].[AssignedCityName], [o].[CityOfBirthName], [o].[FullName], [o].[HasSoulPatch], [o].[LeaderNickname], [o].[LeaderSquadId], [o].[Rank], N'Officer' AS [Discriminator] + FROM [Officers] AS [o] +) AS [t] +ORDER BY [t].[FullName] +OFFSET @__p_0 ROWS FETCH NEXT 1 ROWS ONLY +"""); + } + + public override async Task ElementAtOrDefault_basic_with_OrderBy(bool async) + { + await base.ElementAtOrDefault_basic_with_OrderBy(async); + + AssertSql( +""" +@__p_0='1' + +SELECT [t].[Nickname], [t].[SquadId], [t].[AssignedCityName], [t].[CityOfBirthName], [t].[FullName], [t].[HasSoulPatch], [t].[LeaderNickname], [t].[LeaderSquadId], [t].[Rank], [t].[Discriminator] +FROM ( + SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank], N'Gear' AS [Discriminator] + FROM [Gears] AS [g] + UNION ALL + SELECT [o].[Nickname], [o].[SquadId], [o].[AssignedCityName], [o].[CityOfBirthName], [o].[FullName], [o].[HasSoulPatch], [o].[LeaderNickname], [o].[LeaderSquadId], [o].[Rank], N'Officer' AS [Discriminator] + FROM [Officers] AS [o] +) AS [t] +ORDER BY [t].[FullName] +OFFSET @__p_0 ROWS FETCH NEXT 1 ROWS ONLY +"""); + } + + public override async Task ElementAtOrDefault_basic_with_OrderBy_parameter(bool async) + { + await base.ElementAtOrDefault_basic_with_OrderBy_parameter(async); + + AssertSql( +""" +@__p_0='2' + +SELECT [t].[Nickname], [t].[SquadId], [t].[AssignedCityName], [t].[CityOfBirthName], [t].[FullName], [t].[HasSoulPatch], [t].[LeaderNickname], [t].[LeaderSquadId], [t].[Rank], [t].[Discriminator] +FROM ( + SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank], N'Gear' AS [Discriminator] + FROM [Gears] AS [g] + UNION ALL + SELECT [o].[Nickname], [o].[SquadId], [o].[AssignedCityName], [o].[CityOfBirthName], [o].[FullName], [o].[HasSoulPatch], [o].[LeaderNickname], [o].[LeaderSquadId], [o].[Rank], N'Officer' AS [Discriminator] + FROM [Officers] AS [o] +) AS [t] +ORDER BY [t].[FullName] +OFFSET @__p_0 ROWS FETCH NEXT 1 ROWS ONLY +"""); + } + + public override async Task Where_subquery_with_ElementAtOrDefault_equality_to_null_with_composite_key(bool async) + { + await base.Where_subquery_with_ElementAtOrDefault_equality_to_null_with_composite_key(async); + + AssertSql( +""" +SELECT [s].[Id], [s].[Banner], [s].[Banner5], [s].[InternalNumber], [s].[Name] +FROM [Squads] AS [s] +WHERE NOT (EXISTS ( + SELECT 1 + FROM ( + SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank], N'Gear' AS [Discriminator] + FROM [Gears] AS [g] + UNION ALL + SELECT [o].[Nickname], [o].[SquadId], [o].[AssignedCityName], [o].[CityOfBirthName], [o].[FullName], [o].[HasSoulPatch], [o].[LeaderNickname], [o].[LeaderSquadId], [o].[Rank], N'Officer' AS [Discriminator] + FROM [Officers] AS [o] + ) AS [t] + WHERE [s].[Id] = [t].[SquadId] + ORDER BY [t].[Nickname] + OFFSET 2 ROWS)) +"""); + } + + public override async Task Where_subquery_with_ElementAt_using_column_as_index(bool async) + { + await base.Where_subquery_with_ElementAt_using_column_as_index(async); + + AssertSql( +""" +SELECT [s].[Id], [s].[Banner], [s].[Banner5], [s].[InternalNumber], [s].[Name] +FROM [Squads] AS [s] +WHERE ( + SELECT [t].[Nickname] + FROM ( + SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank], N'Gear' AS [Discriminator] + FROM [Gears] AS [g] + UNION ALL + SELECT [o].[Nickname], [o].[SquadId], [o].[AssignedCityName], [o].[CityOfBirthName], [o].[FullName], [o].[HasSoulPatch], [o].[LeaderNickname], [o].[LeaderSquadId], [o].[Rank], N'Officer' AS [Discriminator] + FROM [Officers] AS [o] + ) AS [t] + WHERE [s].[Id] = [t].[SquadId] + ORDER BY [t].[Nickname] + OFFSET [s].[Id] ROWS FETCH NEXT 1 ROWS ONLY) = N'Cole Train' +"""); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs index f707ac2f47a..46434593a10 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs @@ -11198,6 +11198,96 @@ WHERE [o].[Nickname] IS NOT NULL """); } + public override async Task ElementAt_basic_with_OrderBy(bool async) + { + await base.ElementAt_basic_with_OrderBy(async); + + AssertSql( +""" +@__p_0='0' + +SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank], CASE + WHEN [o].[Nickname] IS NOT NULL THEN N'Officer' +END AS [Discriminator] +FROM [Gears] AS [g] +LEFT JOIN [Officers] AS [o] ON [g].[Nickname] = [o].[Nickname] AND [g].[SquadId] = [o].[SquadId] +ORDER BY [g].[FullName] +OFFSET @__p_0 ROWS FETCH NEXT 1 ROWS ONLY +"""); + } + + public override async Task ElementAtOrDefault_basic_with_OrderBy(bool async) + { + await base.ElementAtOrDefault_basic_with_OrderBy(async); + + AssertSql( +""" +@__p_0='1' + +SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank], CASE + WHEN [o].[Nickname] IS NOT NULL THEN N'Officer' +END AS [Discriminator] +FROM [Gears] AS [g] +LEFT JOIN [Officers] AS [o] ON [g].[Nickname] = [o].[Nickname] AND [g].[SquadId] = [o].[SquadId] +ORDER BY [g].[FullName] +OFFSET @__p_0 ROWS FETCH NEXT 1 ROWS ONLY +"""); + } + + public override async Task ElementAtOrDefault_basic_with_OrderBy_parameter(bool async) + { + await base.ElementAtOrDefault_basic_with_OrderBy_parameter(async); + + AssertSql( +""" +@__p_0='2' + +SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank], CASE + WHEN [o].[Nickname] IS NOT NULL THEN N'Officer' +END AS [Discriminator] +FROM [Gears] AS [g] +LEFT JOIN [Officers] AS [o] ON [g].[Nickname] = [o].[Nickname] AND [g].[SquadId] = [o].[SquadId] +ORDER BY [g].[FullName] +OFFSET @__p_0 ROWS FETCH NEXT 1 ROWS ONLY +"""); + } + + public override async Task Where_subquery_with_ElementAtOrDefault_equality_to_null_with_composite_key(bool async) + { + await base.Where_subquery_with_ElementAtOrDefault_equality_to_null_with_composite_key(async); + + AssertSql( +""" +SELECT [s].[Id], [s].[Banner], [s].[Banner5], [s].[InternalNumber], [s].[Name] +FROM [Squads] AS [s] +WHERE NOT (EXISTS ( + SELECT 1 + FROM [Gears] AS [g] + LEFT JOIN [Officers] AS [o] ON [g].[Nickname] = [o].[Nickname] AND [g].[SquadId] = [o].[SquadId] + WHERE [s].[Id] = [g].[SquadId] + ORDER BY [g].[Nickname] + OFFSET 2 ROWS)) +"""); + } + + public override async Task Where_subquery_with_ElementAt_using_column_as_index(bool async) + { + await base.Where_subquery_with_ElementAt_using_column_as_index(async); + + AssertSql( +""" +SELECT [s].[Id], [s].[Banner], [s].[Banner5], [s].[InternalNumber], [s].[Name] +FROM [Squads] AS [s] +WHERE ( + SELECT [g].[Nickname] + FROM [Gears] AS [g] + LEFT JOIN [Officers] AS [o] ON [g].[Nickname] = [o].[Nickname] AND [g].[SquadId] = [o].[SquadId] + WHERE [s].[Id] = [g].[SquadId] + ORDER BY [g].[Nickname] + OFFSET [s].[Id] ROWS FETCH NEXT 1 ROWS ONLY) = N'Cole Train' +"""); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalComplexNavigationsCollectionsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalComplexNavigationsCollectionsQuerySqlServerTest.cs index 23a82d8e294..0850234d7f5 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalComplexNavigationsCollectionsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalComplexNavigationsCollectionsQuerySqlServerTest.cs @@ -20,6 +20,8 @@ public TemporalComplexNavigationsCollectionsQuerySqlServerTest( protected override Expression RewriteServerQueryExpression(Expression serverQueryExpression) { + serverQueryExpression = base.RewriteServerQueryExpression(serverQueryExpression); + var temporalEntityTypes = new List { typeof(Level1), diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalComplexNavigationsCollectionsSharedTypeQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalComplexNavigationsCollectionsSharedTypeQuerySqlServerTest.cs index 530015ae98b..e1a93618956 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalComplexNavigationsCollectionsSharedTypeQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalComplexNavigationsCollectionsSharedTypeQuerySqlServerTest.cs @@ -20,6 +20,8 @@ public TemporalComplexNavigationsCollectionsSharedTypeQuerySqlServerTest( protected override Expression RewriteServerQueryExpression(Expression serverQueryExpression) { + serverQueryExpression = base.RewriteServerQueryExpression(serverQueryExpression); + var temporalEntityTypes = new List { typeof(Level1), diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalFiltersInheritanceQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalFiltersInheritanceQuerySqlServerTest.cs index e99be2c055e..3c922201d74 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalFiltersInheritanceQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalFiltersInheritanceQuerySqlServerTest.cs @@ -20,6 +20,8 @@ public TemporalFiltersInheritanceQuerySqlServerTest( protected override Expression RewriteServerQueryExpression(Expression serverQueryExpression) { + serverQueryExpression = base.RewriteServerQueryExpression(serverQueryExpression); + var temporalEntityTypes = new List { typeof(Animal), diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs index 4f621c11894..9d6acb19d37 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs @@ -21,6 +21,8 @@ public TemporalGearsOfWarQuerySqlServerTest(TemporalGearsOfWarQuerySqlServerFixt protected override Expression RewriteServerQueryExpression(Expression serverQueryExpression) { + serverQueryExpression = base.RewriteServerQueryExpression(serverQueryExpression); + var temporalEntityTypes = new List { typeof(City), @@ -9792,6 +9794,74 @@ public override async Task EF_Property_based_Include_navigation_on_derived_type( """); } + // Sequence contains no elements due to temporal filter + public override Task ElementAt_basic_with_OrderBy(bool async) + => Task.CompletedTask; + + public override async Task ElementAtOrDefault_basic_with_OrderBy(bool async) + { + await base.ElementAtOrDefault_basic_with_OrderBy(async); + + AssertSql( +""" +@__p_0='1' + +SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[PeriodEnd], [g].[PeriodStart], [g].[Rank] +FROM [Gears] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [g] +ORDER BY [g].[FullName] +OFFSET @__p_0 ROWS FETCH NEXT 1 ROWS ONLY +"""); + } + + public override async Task ElementAtOrDefault_basic_with_OrderBy_parameter(bool async) + { + await base.ElementAtOrDefault_basic_with_OrderBy_parameter(async); + + AssertSql( +""" +@__p_0='2' + +SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[PeriodEnd], [g].[PeriodStart], [g].[Rank] +FROM [Gears] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [g] +ORDER BY [g].[FullName] +OFFSET @__p_0 ROWS FETCH NEXT 1 ROWS ONLY +"""); + } + + public override async Task Where_subquery_with_ElementAtOrDefault_equality_to_null_with_composite_key(bool async) + { + await base.Where_subquery_with_ElementAtOrDefault_equality_to_null_with_composite_key(async); + + AssertSql( +""" +SELECT [s].[Id], [s].[Banner], [s].[Banner5], [s].[InternalNumber], [s].[Name], [s].[PeriodEnd], [s].[PeriodStart] +FROM [Squads] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [s] +WHERE NOT (EXISTS ( + SELECT 1 + FROM [Gears] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [g] + WHERE [s].[Id] = [g].[SquadId] + ORDER BY [g].[Nickname] + OFFSET 2 ROWS)) +"""); + } + + public override async Task Where_subquery_with_ElementAt_using_column_as_index(bool async) + { + await base.Where_subquery_with_ElementAt_using_column_as_index(async); + + AssertSql( +""" +SELECT [s].[Id], [s].[Banner], [s].[Banner5], [s].[InternalNumber], [s].[Name], [s].[PeriodEnd], [s].[PeriodStart] +FROM [Squads] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [s] +WHERE ( + SELECT [g].[Nickname] + FROM [Gears] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [g] + WHERE [s].[Id] = [g].[SquadId] + ORDER BY [g].[Nickname] + OFFSET [s].[Id] ROWS FETCH NEXT 1 ROWS ONLY) = N'Cole Train' +"""); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalManyToManyQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalManyToManyQuerySqlServerTest.cs index 929c30c7513..4a6acd18c37 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalManyToManyQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalManyToManyQuerySqlServerTest.cs @@ -19,6 +19,8 @@ public TemporalManyToManyQuerySqlServerTest(TemporalManyToManyQuerySqlServerFixt protected override Expression RewriteServerQueryExpression(Expression serverQueryExpression) { + serverQueryExpression = base.RewriteServerQueryExpression(serverQueryExpression); + var temporalEntityTypes = new List { typeof(EntityOne), diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalOwnedQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalOwnedQuerySqlServerTest.cs index 9b322cc1b7d..2b5b3ffb323 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalOwnedQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalOwnedQuerySqlServerTest.cs @@ -18,6 +18,8 @@ protected override bool CanExecuteQueryString protected override Expression RewriteServerQueryExpression(Expression serverQueryExpression) { + serverQueryExpression = base.RewriteServerQueryExpression(serverQueryExpression); + var temporalEntityTypes = new List { typeof(OwnedPerson), diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs index 6db8f079775..48091ecfc76 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore.Sqlite.Internal; using Microsoft.EntityFrameworkCore.TestModels.GearsOfWarModel; @@ -9350,6 +9351,88 @@ public override async Task EF_Property_based_Include_navigation_on_derived_type( """); } + public override async Task ElementAt_basic_with_OrderBy(bool async) + { + await base.ElementAt_basic_with_OrderBy(async); + + AssertSql( +""" +@__p_0='0' + +SELECT "g"."Nickname", "g"."SquadId", "g"."AssignedCityName", "g"."CityOfBirthName", "g"."Discriminator", "g"."FullName", "g"."HasSoulPatch", "g"."LeaderNickname", "g"."LeaderSquadId", "g"."Rank" +FROM "Gears" AS "g" +ORDER BY "g"."FullName" +LIMIT 1 OFFSET @__p_0 +"""); + } + + public override async Task ElementAtOrDefault_basic_with_OrderBy(bool async) + { + await base.ElementAtOrDefault_basic_with_OrderBy(async); + + AssertSql( +""" +@__p_0='1' + +SELECT "g"."Nickname", "g"."SquadId", "g"."AssignedCityName", "g"."CityOfBirthName", "g"."Discriminator", "g"."FullName", "g"."HasSoulPatch", "g"."LeaderNickname", "g"."LeaderSquadId", "g"."Rank" +FROM "Gears" AS "g" +ORDER BY "g"."FullName" +LIMIT 1 OFFSET @__p_0 +"""); + } + + public override async Task ElementAtOrDefault_basic_with_OrderBy_parameter(bool async) + { + await base.ElementAtOrDefault_basic_with_OrderBy_parameter(async); + + AssertSql( +""" +@__p_0='2' + +SELECT "g"."Nickname", "g"."SquadId", "g"."AssignedCityName", "g"."CityOfBirthName", "g"."Discriminator", "g"."FullName", "g"."HasSoulPatch", "g"."LeaderNickname", "g"."LeaderSquadId", "g"."Rank" +FROM "Gears" AS "g" +ORDER BY "g"."FullName" +LIMIT 1 OFFSET @__p_0 +"""); + } + + public override async Task Where_subquery_with_ElementAtOrDefault_equality_to_null_with_composite_key(bool async) + { + await base.Where_subquery_with_ElementAtOrDefault_equality_to_null_with_composite_key(async); + + AssertSql( +""" +SELECT "s"."Id", "s"."Banner", "s"."Banner5", "s"."InternalNumber", "s"."Name" +FROM "Squads" AS "s" +WHERE NOT (EXISTS ( + SELECT 1 + FROM "Gears" AS "g" + WHERE "s"."Id" = "g"."SquadId" + ORDER BY "g"."Nickname" + LIMIT -1 OFFSET 2)) +"""); + } + + public override async Task Where_subquery_with_ElementAt_using_column_as_index(bool async) + { + var message = (await Assert.ThrowsAsync( + () => base.Where_subquery_with_ElementAt_using_column_as_index(async))).Message; + + Assert.Equal("SQLite Error 1: 'no such column: s.Id'.", message); + + AssertSql( +""" +SELECT "s"."Id", "s"."Banner", "s"."Banner5", "s"."InternalNumber", "s"."Name" +FROM "Squads" AS "s" +WHERE ( + SELECT "g"."Nickname" + FROM "Gears" AS "g" + WHERE "s"."Id" = "g"."SquadId" + ORDER BY "g"."Nickname" + LIMIT 1 OFFSET "s"."Id") = 'Cole Train' +"""); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); } diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/TPCGearsOfWarQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/TPCGearsOfWarQuerySqliteTest.cs index 2e12f285fa9..af9fc4e0122 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/TPCGearsOfWarQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/TPCGearsOfWarQuerySqliteTest.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore.Sqlite.Internal; namespace Microsoft.EntityFrameworkCore.Query; @@ -394,6 +395,32 @@ public override Task Where_TimeOnly_subtract_TimeOnly(bool async) // TimeSpan. Issue #18844. => AssertTranslationFailed(() => base.Where_TimeOnly_subtract_TimeOnly(async)); + public override async Task Where_subquery_with_ElementAt_using_column_as_index(bool async) + { + var message = (await Assert.ThrowsAsync( + () => base.Where_subquery_with_ElementAt_using_column_as_index(async))).Message; + + Assert.Equal("SQLite Error 1: 'no such column: s.Id'.", message); + + AssertSql( +""" +SELECT "s"."Id", "s"."Banner", "s"."Banner5", "s"."InternalNumber", "s"."Name" +FROM "Squads" AS "s" +WHERE ( + SELECT "t"."Nickname" + FROM ( + SELECT "g"."Nickname", "g"."SquadId", "g"."AssignedCityName", "g"."CityOfBirthName", "g"."FullName", "g"."HasSoulPatch", "g"."LeaderNickname", "g"."LeaderSquadId", "g"."Rank", 'Gear' AS "Discriminator" + FROM "Gears" AS "g" + UNION ALL + SELECT "o"."Nickname", "o"."SquadId", "o"."AssignedCityName", "o"."CityOfBirthName", "o"."FullName", "o"."HasSoulPatch", "o"."LeaderNickname", "o"."LeaderSquadId", "o"."Rank", 'Officer' AS "Discriminator" + FROM "Officers" AS "o" + ) AS "t" + WHERE "s"."Id" = "t"."SquadId" + ORDER BY "t"."Nickname" + LIMIT 1 OFFSET "s"."Id") = 'Cole Train' +"""); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); } diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/TPTGearsOfWarQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/TPTGearsOfWarQuerySqliteTest.cs index f18b7a6c8bb..4727d9b01ec 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/TPTGearsOfWarQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/TPTGearsOfWarQuerySqliteTest.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore.Sqlite.Internal; namespace Microsoft.EntityFrameworkCore.Query; @@ -394,6 +395,27 @@ public override Task Where_TimeOnly_subtract_TimeOnly(bool async) // TimeSpan. Issue #18844. => AssertTranslationFailed(() => base.Where_TimeOnly_subtract_TimeOnly(async)); + public override async Task Where_subquery_with_ElementAt_using_column_as_index(bool async) + { + var message = (await Assert.ThrowsAsync( + () => base.Where_subquery_with_ElementAt_using_column_as_index(async))).Message; + + Assert.Equal("SQLite Error 1: 'no such column: s.Id'.", message); + + AssertSql( +""" +SELECT "s"."Id", "s"."Banner", "s"."Banner5", "s"."InternalNumber", "s"."Name" +FROM "Squads" AS "s" +WHERE ( + SELECT "g"."Nickname" + FROM "Gears" AS "g" + LEFT JOIN "Officers" AS "o" ON "g"."Nickname" = "o"."Nickname" AND "g"."SquadId" = "o"."SquadId" + WHERE "s"."Id" = "g"."SquadId" + ORDER BY "g"."Nickname" + LIMIT 1 OFFSET "s"."Id") = 'Cole Train' +"""); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); }