Skip to content
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

Cursor-based paging WIP. #14971

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions src/EFCore/EntityFrameworkQueryableExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -690,6 +690,103 @@ public static Task<TSource> SingleOrDefaultAsync<TSource>(

#endregion

#region TakeAfterMatch

internal static readonly MethodInfo TakeAfterMatchMethodInfo
= typeof(EntityFrameworkQueryableExtensions)
.GetTypeInfo().GetDeclaredMethod(nameof(TakeAfterMatch));

/// <summary>
/// Returns a new query where the results will the next <paramref name="count"/>
/// elements after the first element that matches <paramref name="predicate"/>.
/// </summary>
/// <typeparam name="TSource">
/// The type of the elements of <paramref name="source"/>.
/// </typeparam>
/// <param name="source">
/// An <see cref="IQueryable{T}" /> to return results from.
/// </param>
/// <param name="predicate">
/// An function to test an element for the first match.
/// </param>
/// <param name="count">
/// The number of elements to return.
/// </param>
/// <returns>
/// A new query where the result set will be the next <paramref name="count"/>
/// elements after the first element that matches <paramref name="predicate"/>.
/// </returns>
public static IQueryable<TSource> TakeAfterMatch<TSource>(
[NotNull] this IQueryable<TSource> source,
[NotNull] Expression<Func<TSource, bool>> predicate,
[NotNull] int count)
where TSource : class
{
Check.NotNull(source, nameof(source));
Check.NotNull(predicate, nameof(predicate));
Check.NotNull(count, nameof(count));

return
source.Provider is EntityQueryProvider
? source.Provider.CreateQuery<TSource>(
Expression.Call(
instance: null,
method: TakeAfterMatchMethodInfo.MakeGenericMethod(typeof(TSource)),
arguments: new[] { source.Expression, predicate, Expression.Constant(count, typeof(int)) }))
: source;
}

#endregion

#region TakeBeforeMatch

internal static readonly MethodInfo TakeBeforeMatchMethodInfo
= typeof(EntityFrameworkQueryableExtensions)
.GetTypeInfo().GetDeclaredMethod(nameof(TakeBeforeMatch));

/// <summary>
/// Returns a new query where the results will the previous <paramref name="count"/>
/// elements before the first element that matches <paramref name="predicate"/>.
/// </summary>
/// <typeparam name="TSource">
/// The type of the elements of <paramref name="source"/>.
/// </typeparam>
/// <param name="source">
/// An <see cref="IQueryable{T}" /> to return results from.
/// </param>
/// <param name="predicate">
/// An function to test an element for the first match.
/// </param>
/// <param name="count">
/// The number of elements to return.
/// </param>
/// <returns>
/// A new query where the result set will be the previous <paramref name="count"/>
/// elements before the first element that matches <paramref name="predicate"/>.
/// </returns>
public static IQueryable<TSource> TakeBeforeMatch<TSource>(
[NotNull] this IQueryable<TSource> source,
[NotNull] Expression<Func<TSource, bool>> predicate,
[NotNull] int count)
where TSource : class
{
Check.NotNull(source, nameof(source));
Check.NotNull(predicate, nameof(predicate));
Check.NotNull(count, nameof(count));

return
source.Provider is EntityQueryProvider
? source.Provider.CreateQuery<TSource>(
Expression.Call(
instance: null,
method: TakeBeforeMatchMethodInfo.MakeGenericMethod(typeof(TSource)),
arguments: new[] { source.Expression, predicate, Expression.Constant(count, typeof(int)) }))
: source;
}

#endregion


#region Min

private static readonly MethodInfo _min = GetMethod(nameof(Queryable.Min), predicate: mi => mi.IsGenericMethod);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ public MethodInfoBasedNodeTypeRegistryFactory(
.Register(StringIncludeExpressionNode.SupportedMethods, typeof(StringIncludeExpressionNode));
_methodInfoBasedNodeTypeRegistry
.Register(ThenIncludeExpressionNode.SupportedMethods, typeof(ThenIncludeExpressionNode));
_methodInfoBasedNodeTypeRegistry
.Register(TakeBesideMatchExpressionNode.SupportedMethods, typeof(TakeBesideMatchExpressionNode));

_nodeTypeProviders = new INodeTypeProvider[] { _methodInfoBasedNodeTypeRegistry, _methodNameBasedNodeTypeRegistry };
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ public class IncludeResultOperator : SequenceTypePreservingResultOperatorBase, I
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public IncludeResultOperator(
[NotNull] INavigation[] navigationPath, [NotNull] Expression pathFromQuerySource, bool implicitLoad = false)
[NotNull] INavigation[] navigationPath,
[NotNull] Expression pathFromQuerySource,
bool implicitLoad = false)
{
_navigationPaths = new List<INavigation[]>
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;
using JetBrains.Annotations;
using Remotion.Linq.Clauses;
using Remotion.Linq.Parsing.Structure.IntermediateModel;

namespace Microsoft.EntityFrameworkCore.Query.ResultOperators.Internal
{
/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public class TakeBesideMatchExpressionNode : ResultOperatorExpressionNodeBase
{
/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public static readonly IReadOnlyCollection<MethodInfo> SupportedMethods = new[]
{
EntityFrameworkQueryableExtensions.TakeAfterMatchMethodInfo,
EntityFrameworkQueryableExtensions.TakeBeforeMatchMethodInfo
};

private readonly LambdaExpression _lambdaExpression;

private readonly Expression _count;

/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public TakeBesideMatchExpressionNode(
MethodCallExpressionParseInfo parseInfo,
[NotNull] LambdaExpression predicateLambda,
[NotNull] Expression count)
: base(parseInfo, predicateLambda, null)
{
_count = count;
}

/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
protected override ResultOperatorBase CreateResultOperator(ClauseGenerationContext clauseGenerationContext)
{
var takeBesideMatchOperator = new TakeBesideMatchResultOperator(
forward: ParsedExpression.Method.GetGenericMethodDefinition()
.Equals(EntityFrameworkQueryableExtensions.TakeAfterMatchMethodInfo)
);
// etc...
return takeBesideMatchOperator;
}

/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public override Expression Resolve(
ParameterExpression inputParameter,
Expression expressionToBeResolved,
ClauseGenerationContext clauseGenerationContext)
=> Source.Resolve(inputParameter, expressionToBeResolved, clauseGenerationContext);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Linq.Expressions;
using JetBrains.Annotations;
using Remotion.Linq;
using Remotion.Linq.Clauses;
using Remotion.Linq.Clauses.ResultOperators;
using Remotion.Linq.Clauses.StreamedData;

namespace Microsoft.EntityFrameworkCore.Query.ResultOperators.Internal
{
/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public class TakeBesideMatchResultOperator : SequenceTypePreservingResultOperatorBase, IQueryAnnotation
{
private Expression _count;

/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public TakeBesideMatchResultOperator(bool forward, LambdaExpression predicate, Expression count)
{
IsForward = forward;

Count = count;
}



public Expression Count
{
get { return _count; }
set
{
//ArgumentUtility.CheckNotNull("value", value);
if (value.Type != typeof(int))
{
var message = string.Format("The value expression returns '{0}', an expression returning 'System.Int32' was expected.", value.Type);
throw new ArgumentException(message, "value");
}

_count = value;
}
}

/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public virtual IQuerySource QuerySource { get; [NotNull] set; }

/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public virtual QueryModel QueryModel { get; [NotNull] set; }

/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public virtual bool IsForward { get; }

/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public override string ToString()
{
var paramsString = $"TODO,Count.BuildString()}";
return IsForward
? $"TakeAfterMatch({paramsString})"
: $"TakeBeforeMatch({paramsString})";
}

/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public override ResultOperatorBase Clone(CloneContext cloneContext)
=> new TakeBesideMatchResultOperator(IsForward, );

/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public override void TransformExpressions(Func<Expression, Expression> transformation)
{
}

/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public override StreamedSequence ExecuteInMemory<T>(StreamedSequence input)
{
var sequence = input.GetTypedSequence<T>();
var match = sequence.FirstOrDefault() // TODO: Should use SingleOrDefault()?
}
}
}
43 changes: 43 additions & 0 deletions test/EFCore.SqlServer.FunctionalTests/CursorBasedPagingTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore.TestModels.GearsOfWarModel;
using Microsoft.EntityFrameworkCore.TestUtilities.Xunit;
using Xunit;
using Xunit.Abstractions;

namespace Microsoft.EntityFrameworkCore.Query
{
public class CursorBasedPagingTest : QueryTestBase<GearsOfWarQuerySqlServerFixture>, IDisposable
{
public CursorBasedPagingTest(GearsOfWarQuerySqlServerFixture fixture, ITestOutputHelper testOutputHelper)
: base(fixture)
{
Fixture.TestSqlLoggerFactory.Clear();
//Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper);
}

public void Dispose()
{

}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public async Task TakeAfterMatch(bool isAsync)
{
await AssertQuery<Gear>(
isAsync,
gs => gs.TakeAfterMatch(g => g.IsMarcus, 5));

AssertSql(
@"xyz");
}

private void AssertSql(params string[] expected)
=> Fixture.TestSqlLoggerFactory.AssertBaseline(expected);
}
}