From fc8fbcd53ac5d8fc3f9f206f139850d9ac97db02 Mon Sep 17 00:00:00 2001 From: Brandon Bernard Date: Fri, 23 Apr 2021 01:23:06 -0500 Subject: [PATCH] Work in progress for RepoDb offset paging, but BatchQuery is not working as originally thought, will have to re-factor to implement a Skip/Take query manually! --- .../GraphQLRepoDbMapper.cs | 7 ++- .../IRepoDbOffsetPagingParams.cs | 1 + .../RepoDbBatchQueryExtensions.cs | 51 ++++++++++++++++--- .../RepoDbOffsetPagingParams.cs | 10 ++-- .../Characters/CharacterQueries.cs | 4 +- .../Repositories/CharacterRepository.cs | 18 ++++--- Temp/RepoDB Batch Offset Paging Tests.sql | 13 +++++ 7 files changed, 84 insertions(+), 20 deletions(-) create mode 100644 Temp/RepoDB Batch Offset Paging Tests.sql diff --git a/GraphQL.RepoDb.SqlServer/GraphQLRepoDbMapper.cs b/GraphQL.RepoDb.SqlServer/GraphQLRepoDbMapper.cs index 6181dfc..fa1e935 100644 --- a/GraphQL.RepoDb.SqlServer/GraphQLRepoDbMapper.cs +++ b/GraphQL.RepoDb.SqlServer/GraphQLRepoDbMapper.cs @@ -194,7 +194,12 @@ public RepoDbOffsetPagingParams GetOffsetPagingParameters() return null; } - return RepoDbOffsetPagingParams.FromSkipTake(graphQLPagingArgs.Skip, graphQLPagingArgs.Take); + + return RepoDbOffsetPagingParams.FromSkipTake( + graphQLPagingArgs.Skip, + graphQLPagingArgs.Take, + this.GraphQLParamsContext.IsTotalCountRequested + ); } } } diff --git a/GraphQL.RepoDb.SqlServer/RepoDb.OffsetPagination/IRepoDbOffsetPagingParams.cs b/GraphQL.RepoDb.SqlServer/RepoDb.OffsetPagination/IRepoDbOffsetPagingParams.cs index ab71cc7..3b95941 100644 --- a/GraphQL.RepoDb.SqlServer/RepoDb.OffsetPagination/IRepoDbOffsetPagingParams.cs +++ b/GraphQL.RepoDb.SqlServer/RepoDb.OffsetPagination/IRepoDbOffsetPagingParams.cs @@ -4,5 +4,6 @@ public interface IRepoDbOffsetPagingParams { int Page { get; } int RowsPerBatch { get; } + bool IsTotalCountEnabled { get; } } } \ No newline at end of file diff --git a/GraphQL.RepoDb.SqlServer/RepoDb.OffsetPagination/RepoDbBatchQueryExtensions.cs b/GraphQL.RepoDb.SqlServer/RepoDb.OffsetPagination/RepoDbBatchQueryExtensions.cs index b99727b..bb9e0d7 100644 --- a/GraphQL.RepoDb.SqlServer/RepoDb.OffsetPagination/RepoDbBatchQueryExtensions.cs +++ b/GraphQL.RepoDb.SqlServer/RepoDb.OffsetPagination/RepoDbBatchQueryExtensions.cs @@ -11,7 +11,7 @@ using System.Threading.Tasks; using RepoDb.Interfaces; -namespace RepoDb.CursorPagination +namespace RepoDb.OffsetPagination { public static class BaseRepositoryOffsetPaginationCustomExtensions { @@ -25,6 +25,7 @@ public static class BaseRepositoryOffsetPaginationCustomExtensions /// /// /// + /// /// /// /// @@ -40,6 +41,7 @@ public static async Task> GraphQLBatchOffsetPagingQue Expression> where, int? page = null, int? rowsPerBatch = null, + bool fetchTotalCount = false, string tableName = null, string hints = null, IEnumerable fields = null, @@ -55,6 +57,7 @@ public static async Task> GraphQLBatchOffsetPagingQue return await baseRepo.GraphQLBatchOffsetPagingQueryAsync( page: page, rowsPerBatch: rowsPerBatch, + fetchTotalCount: fetchTotalCount, orderBy: orderBy, where: where != null ? QueryGroup.Parse(where) : (QueryGroup)null, hints: hints, @@ -78,6 +81,7 @@ public static async Task> GraphQLBatchOffsetPagingQue /// /// /// + /// /// /// /// @@ -91,6 +95,7 @@ public static async Task> GraphQLBatchOffsetPagingQue QueryGroup where = null, int? page = null, int? rowsPerBatch = null, + bool fetchTotalCount = false, string hints = null, IEnumerable fields = null, string tableName = null, @@ -111,6 +116,7 @@ public static async Task> GraphQLBatchOffsetPagingQue var cursorPageResult = await connection.GraphQLBatchOffsetPagingQueryAsync( page: page, rowsPerBatch: rowsPerBatch, + fetchTotalCount: fetchTotalCount, orderBy: orderBy, where: where, hints: hints, @@ -145,6 +151,7 @@ public static async Task> GraphQLBatchOffsetPagingQue /// /// /// + /// /// /// /// @@ -159,6 +166,7 @@ public static async Task> GraphQLBatchOffsetPagingQue Expression> where, int? page = null, int? rowsPerBatch = null, + bool fetchTotalCount = false, string hints = null, IEnumerable fields = null, int? commandTimeout = null, @@ -172,6 +180,7 @@ public static async Task> GraphQLBatchOffsetPagingQue return await dbConnection.GraphQLBatchOffsetPagingQueryAsync( page: page, rowsPerBatch: rowsPerBatch, + fetchTotalCount: fetchTotalCount, orderBy: orderBy, where: where != null ? QueryGroup.Parse(where) : (QueryGroup)null, hints: hints, @@ -193,6 +202,7 @@ public static async Task> GraphQLBatchOffsetPagingQue /// /// /// + /// /// /// /// @@ -206,6 +216,7 @@ public static async Task> GraphQLBatchOffsetPagingQue QueryGroup where = null, int? page = null, int? rowsPerBatch = null, + bool fetchTotalCount = false, string hints = null, IEnumerable fields = null, string tableName = null, @@ -242,10 +253,14 @@ public static async Task> GraphQLBatchOffsetPagingQue ? RepoDbQueryGroupProxy.GetMappedParamsObject(where) : null; - var batchResults = await dbConnection.BatchQueryAsync( - tableName: tableName, + //We increment Rows count to actually be fetched by One to help determine if there is any data after the current page... + var rowsPerPage = rowsPerBatch ?? int.MaxValue; + var rowsToFetch = rowsPerPage < int.MaxValue ? rowsPerPage + 1 : rowsPerPage; + + var batchResults = (await dbConnection.BatchQueryAsync( + tableName: dbTableName, page: page ?? 0, - rowsPerBatch: rowsPerBatch ?? int.MaxValue, + rowsPerBatch: rowsToFetch, fields: validSelectFields, orderBy: orderBy, where: whereParams, @@ -254,10 +269,34 @@ public static async Task> GraphQLBatchOffsetPagingQue transaction: transaction, trace: trace, cancellationToken: cancellationToken - ).ConfigureAwait(false); + ).ConfigureAwait(false)) + ?.ToList() ?? new List(); + + //If specified we need to get the Total Count... + int totalCount = 0; + if (fetchTotalCount) + { + var repoDbTotalCount = await dbConnection.CountAsync( + tableName: dbTableName, + where: whereParams, + hints: hints, + commandTimeout: commandTimeout, + transaction: transaction, + trace: trace, + cancellationToken: cancellationToken + ).ConfigureAwait(false); + + totalCount = Convert.ToInt32(repoDbTotalCount); + } //TODO: Implement Logic to determine HasNextPage, HasPreviousPage, and get Total Count or Not! - var offsetPageResults = new OffsetPageResults(batchResults, true, true, 0); + var hasPreviousPage = page > 0; + var hasNextPage = batchResults.Count > rowsPerPage; + + //Trim the results to the exact page size (removing potential additional item). + var pageResults = batchResults.Take(rowsPerPage); + + var offsetPageResults = new OffsetPageResults(pageResults, hasNextPage, hasPreviousPage, totalCount); return offsetPageResults; } diff --git a/GraphQL.RepoDb.SqlServer/RepoDb.OffsetPagination/RepoDbOffsetPagingParams.cs b/GraphQL.RepoDb.SqlServer/RepoDb.OffsetPagination/RepoDbOffsetPagingParams.cs index e4daa8f..01fa42c 100644 --- a/GraphQL.RepoDb.SqlServer/RepoDb.OffsetPagination/RepoDbOffsetPagingParams.cs +++ b/GraphQL.RepoDb.SqlServer/RepoDb.OffsetPagination/RepoDbOffsetPagingParams.cs @@ -17,13 +17,15 @@ public class RepoDbOffsetPagingParams : IRepoDbOffsetPagingParams /// /// Is Optional and may be null to get all results; will default to getting all data (int.MaxValue) if null or is not a valid positive value. /// Is Optional and will default to the first page (zero-based) if null or is not a valid positive value. - public RepoDbOffsetPagingParams(int? rowsPerBatch = null, int? page = null) + /// Enable the retrieval of the Total Count; for OffsetPaging this is optional and enabling it may impact performance. + public RepoDbOffsetPagingParams(int? rowsPerBatch = null, int? page = null, bool fetchTotalCount = false) { if (rowsPerBatch.HasValue && rowsPerBatch <= 0) throw new ArgumentException("A valid number of rows per batch must be specified (>0).", nameof(rowsPerBatch)); this.RowsPerBatch = rowsPerBatch.HasValue && rowsPerBatch > 0 ? (int)rowsPerBatch : int.MaxValue; this.Page = page.HasValue && page > 0 ? (int)page : 0; + this.IsTotalCountEnabled = fetchTotalCount; } /// @@ -32,8 +34,9 @@ public RepoDbOffsetPagingParams(int? rowsPerBatch = null, int? page = null) /// /// Is Optional and may be null to get all results; will default to skipping none (0) if null or is not a valid positive value. /// Is Optional and may be null to get all results; will default to getting all data (int.MaxValue) if null or is not a valid positive value. + /// Enable the retrieval of the Total Count; for OffsetPaging this is optional and enabling it may impact performance. /// - public static RepoDbOffsetPagingParams FromSkipTake(int? skip = null, int? take = null) + public static RepoDbOffsetPagingParams FromSkipTake(int? skip = null, int? take = null, bool fetchTotalCount = false) { if (take.HasValue && take <= 0) throw new ArgumentException("A valid number of items to take (rows-per-page) must be specified (e.g. greater than 0).", nameof(take)); @@ -48,10 +51,11 @@ public static RepoDbOffsetPagingParams FromSkipTake(int? skip = null, int? take var skipOver = Math.Max((skip ?? 0), 0); var page = (int)(skipOver / rowsPerBatch); - return new RepoDbOffsetPagingParams(rowsPerBatch, page); + return new RepoDbOffsetPagingParams(rowsPerBatch, page, fetchTotalCount); } public int Page { get; } public int RowsPerBatch { get; } + public bool IsTotalCountEnabled { get; } } } diff --git a/Sample.StarWars-AzureFunctions-RepoDB/Characters/CharacterQueries.cs b/Sample.StarWars-AzureFunctions-RepoDB/Characters/CharacterQueries.cs index abb288f..5c65c66 100644 --- a/Sample.StarWars-AzureFunctions-RepoDB/Characters/CharacterQueries.cs +++ b/Sample.StarWars-AzureFunctions-RepoDB/Characters/CharacterQueries.cs @@ -55,7 +55,7 @@ [GraphQLParams] IParamsContext graphQLParams // down to the Repository (and underlying Database) layer. var charactersSlice = await repository.GetCursorPagedCharactersAsync( repoDbParams.GetSelectFields(), - repoDbParams.GetSortOrderFields() ?? OrderField.Parse(new { Name = Order.Ascending }), + repoDbParams.GetSortOrderFields(), repoDbParams.GetCursorPagingParameters() ); @@ -91,7 +91,7 @@ [GraphQLParams] IParamsContext graphQLParams // down to the Repository (and underlying Database) layer. var charactersPage = await repository.GetOffsetPagedCharactersAsync( repoDbParams.GetSelectFields(), - repoDbParams.GetSortOrderFields() ?? OrderField.Parse(new { Name = Order.Ascending }), + repoDbParams.GetSortOrderFields(), repoDbParams.GetOffsetPagingParameters() ); diff --git a/Sample.StarWars-AzureFunctions-RepoDB/Repositories/CharacterRepository.cs b/Sample.StarWars-AzureFunctions-RepoDB/Repositories/CharacterRepository.cs index c7ab277..654b16b 100644 --- a/Sample.StarWars-AzureFunctions-RepoDB/Repositories/CharacterRepository.cs +++ b/Sample.StarWars-AzureFunctions-RepoDB/Repositories/CharacterRepository.cs @@ -9,8 +9,9 @@ using HotChocolate.PreProcessingExtensions.Pagination; using Microsoft.Data.SqlClient; using RepoDb; -using RepoDb.CursorPagination; using RepoDb.Enumerations; +using RepoDb.CursorPagination; +using RepoDb.OffsetPagination; using StarWars.Characters; using StarWars.Characters.DbModels; @@ -18,10 +19,10 @@ namespace StarWars.Repositories { public class CharacterRepository : BaseRepository, ICharacterRepository { - public static class TableNames + public static readonly IReadOnlyList DefaultCharacterSortFields = new List() { - public const string StarWarsCharacters = "StarWarsCharacters"; - } + OrderField.Ascending(c => c.Id) + }.AsReadOnly(); public CharacterRepository(string connectionString) : base(connectionString) @@ -38,7 +39,7 @@ IEnumerable sortFields var results = await sqlConn.QueryAsync( where: c => c.Id >= 1000 && c.Id <=2999, fields: selectFields, - orderBy: sortFields + orderBy: sortFields ?? DefaultCharacterSortFields ); var mappedResults = MapDbModelsToCharacterModels(results); @@ -56,7 +57,7 @@ IRepoDbCursorPagingParams pagingParams var pageSlice = await sqlConn.GraphQLBatchSliceQueryAsync( fields: selectFields, - orderBy: sortFields, + orderBy: sortFields ?? DefaultCharacterSortFields, afterCursor: pagingParams.AfterIndex!, beforeCursor: pagingParams.BeforeIndex!, firstTake: pagingParams.First, @@ -80,7 +81,8 @@ IRepoDbOffsetPagingParams pagingParams var offsetPageResults = await sqlConn.GraphQLBatchOffsetPagingQueryAsync( page: pagingParams.Page, rowsPerBatch: pagingParams.RowsPerBatch, - orderBy: sortFields, + fetchTotalCount: pagingParams.IsTotalCountEnabled, + orderBy: sortFields ?? DefaultCharacterSortFields, fields: selectFields ); @@ -97,7 +99,7 @@ IRepoDbCursorPagingParams pagingParams await using var sqlConn = CreateConnection(); var pageSlice = await sqlConn.GraphQLBatchSliceQueryAsync( - orderBy: sortFields, + orderBy: sortFields ?? DefaultCharacterSortFields, fields: selectFields, where: c => c.Id >=1000 && c.Id <= 1999, afterCursor: pagingParams.AfterIndex!, diff --git a/Temp/RepoDB Batch Offset Paging Tests.sql b/Temp/RepoDB Batch Offset Paging Tests.sql new file mode 100644 index 0000000..792feb9 --- /dev/null +++ b/Temp/RepoDB Batch Offset Paging Tests.sql @@ -0,0 +1,13 @@ +SELECT * FROM StarWarsCharacters ORDER BY Id ASC + +DECLARE @page AS INT = 1; +DECLARE @rowsPerBatch AS INT = 8; + +SELECT TOP((@page + 1)*@rowsPerBatch) ROW_NUMBER() OVER (ORDER BY Id ASC) AS [RowNumber], [Id], [Name] FROM [StarWarsCharacters] ORDER BY Id ASC; + +--Modeled from RepoDB's BatchQuery logic (query builder) +WITH CTE AS (SELECT TOP((@page + 1)*@rowsPerBatch) ROW_NUMBER() OVER (ORDER BY Id ASC) AS [RowNumber], [Id], [Name] FROM [StarWarsCharacters] ORDER BY Id ASC) +SELECT [Id], [Name] FROM CTE WHERE ([RowNumber] BETWEEN ((@page * @rowsPerBatch) + 1) AND ((@page + 1) * @rowsPerBatch)); + +SELECT + BetweenSql = 'Between ' + CONVERT(VARCHAR(5), ((@page * @rowsPerBatch) + 1)) + ' and ' + CONVERT(VARCHAR(5), ((@page + 1) * @rowsPerBatch)); \ No newline at end of file