diff --git a/plume-db-querydsl/README.md b/plume-db-querydsl/README.md index 928b485..adf66bd 100644 --- a/plume-db-querydsl/README.md +++ b/plume-db-querydsl/README.md @@ -5,21 +5,21 @@ This module helps integrate [Querydsl SQL](https://github.com/querydsl/querydsl/ with [Plume Database](https://github.com/Coreoz/Plume/tree/master/plume-db). It contains mainly: - `TransactionManagerQuerydsl`: the main class of this module; it will -read the configuration, initialize the SQL connection pool -and provide helper methods to create Querydsl queries, + read the configuration, initialize the SQL connection pool + and provide helper methods to create Querydsl queries, - The generic DAO `CrudDaoQuerydsl` for CRUD operations. Querydsl queries can be created: - **without a `Connection`**: that means that the query will be executed with a connection -from the SQL connection pool. The `Connection` object will be automaticely released in the pool -once the query is executed. + from the SQL connection pool. The `Connection` object will be automaticely released in the pool + once the query is executed. - **with a `Connection`**: that means that the query will be executed on this supplied connection. -This mode is almost always used when a **transaction** is needed: + This mode is almost always used when a **transaction** is needed: ```java transactionManager.execute(connection -> { - transactionManager.insert(QTable.table, connection).populate(bean).execute(); + transactionManager.insert(QTable.table, connection).populate(bean).execute(); transactionManager.delete(QTable.table, connection).where(predicate).execute(); - // the connection is set to autocommit=false and will be commited at the end of the lambda +// the connection is set to autocommit=false and will be commited at the end of the lambda }); ``` @@ -35,15 +35,15 @@ Installation **Maven**: ```xml - com.coreoz - plume-db-querydsl + com.coreoz + plume-db-querydsl - com.coreoz - plume-db-querydsl-codegen - true +com.coreoz +plume-db-querydsl-codegen +true - + ``` **Guice**: `install(new GuiceQuerydslModule());` @@ -64,3 +64,94 @@ Code generation To generate Querydsl entities, a good choice is to use this [Querydsl code generator](https://github.com/Coreoz/Plume/tree/master/plume-db-querydsl-codegen). +Pagination +---------- +### Overview +The `SqlPaginatedQuery` class provides a robust and flexible mechanism for paginating results in a Querydsl query. It abstracts the pagination logic into two generic interfaces —`Slice` and `Page`— which represent paginated results in different ways. + +- `Page`: A `Page` contains a list of results, total count of items, total number of pages, and a flag to indicate if there are more pages available. +- `Slice`: A `Slice` contains a list of results and a flag to indicate if there are more items to be fetched, without calculating the total number of items or pages. + +So using slices will be more efficient than using pages, though the impact will depend on the number of rows to count. +Under the hood: +- When fetching a page, Querydsl will attempt to execute the fetch request and the count request in the same SQL query, if it is not supported by the database, it will execute two queries. +- When fetching a slice of n items, n+1 items will try to be fetched: if the result contains n+1 items, then the `hasMore` attribute will be set to `true` + +Other features: +- **Pagination Logic**: Handles offset-based pagination by calculating the number of records to skip (`offset`) and the number of records to fetch (`limit`) using the page number and page size. +- **Sorting Support**: Allows dynamic sorting of query results by providing an `Expression` and an `Order` (ascending/descending). + +### Working with Pagination from a WebService: +First, you need to create a translation between the API sort key and a table column. +This can be done like this: + +```java +@Getter +public enum UserSortPath { + // users + EMAIL(QUser.user.email), + FIRST_NAME(QUser.user.firstName), + LAST_NAME(QUser.user.lastName), + LAST_LOGIN_DATE(QUser.user.lastLogin), + ; + + private final Expression path; +} +``` + +Then declare your WebService: + +```java +@POST +@Path("/search") +@Operation(description = "Retrieves admin users") +@Consumes(MediaType.APPLICATION_JSON) +public Page searchUsers( + @QueryParam("page") Long page, + @QueryParam("size") Long size, + @QueryParam("sort") String sort, + @QueryParam("sortDirection") Order sortDirection, + UserSearchRequest userSearchRequest +) { + // check the pagination that comes from the API call + if (page < 1) { + throw new WsException(WsError.REQUEST_INVALID, List.of("page")); + } + if (size < 1) { + throw new WsException(WsError.REQUEST_INVALID, List.of("size")); + } + return usersDao.searchUsers( + userSearchRequest, + page, + size, + UserSortPath.valueOf(sort), + sortDirection + ); +} +``` + +Then apply the pagination from the API call with `SqlPaginatedQuery` : + +```java +public Page searchUsers( + UserSearchRequest userSearchRequest, + Long page, + Long size, + Expression path, + Order sortDirection +) { + return SqlPaginatedQuery + .fromQuery( + this.transactionManagerQuerydsl.selectQuery() + .select(QUser.user) + .from(QUser.user) + .where( + QUser.user.firstName.containsIgnoreCase(userSearchRequest.searchText()) + .or(QUser.user.lastName.containsIgnoreCase(userSearchRequest.searchText())) + .or(QUser.user.email.containsIgnoreCase(userSearchRequest.searchText())) + ) + ) + .withSort(path, sortDirection) + .fetchPage(page, size); +} +``` diff --git a/plume-db-querydsl/pom.xml b/plume-db-querydsl/pom.xml index dc27230..44bdc0a 100644 --- a/plume-db-querydsl/pom.xml +++ b/plume-db-querydsl/pom.xml @@ -46,6 +46,13 @@ true + + + org.projectlombok + lombok + provided + + com.coreoz diff --git a/plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/dagger/DaggerQuerydslModule.java b/plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/dagger/DaggerQuerydslModule.java index a313221..3361620 100644 --- a/plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/dagger/DaggerQuerydslModule.java +++ b/plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/dagger/DaggerQuerydslModule.java @@ -1,6 +1,6 @@ package com.coreoz.plume.db.querydsl.dagger; -import javax.inject.Singleton; +import jakarta.inject.Singleton; import com.coreoz.plume.db.querydsl.transaction.TransactionManagerQuerydsl; import com.coreoz.plume.db.transaction.TransactionManager; diff --git a/plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/pagination/SqlPaginatedQuery.java b/plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/pagination/SqlPaginatedQuery.java new file mode 100644 index 0000000..c5ec754 --- /dev/null +++ b/plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/pagination/SqlPaginatedQuery.java @@ -0,0 +1,113 @@ +package com.coreoz.plume.db.querydsl.pagination; + +import com.coreoz.plume.db.pagination.Page; +import com.coreoz.plume.db.pagination.Pages; +import com.coreoz.plume.db.pagination.Slice; +import com.querydsl.core.QueryResults; +import com.querydsl.core.types.Expression; +import com.querydsl.core.types.Order; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.sql.SQLQuery; + +import javax.annotation.Nonnull; +import java.util.List; + +/** + * Paginated query implementation with Querydsl + *
+ * @param The type of elements contained in the request. + *
+ * Usage example: + *
+ * public Page fetchUsers() {
+ *   return SqlPaginatedQuery
+ *             .fromQuery(
+ *                 this.transactionManagerQuerydsl.selectQuery()
+ *                     .select(QUser.user)
+ *                     .from(QUser.user)
+ *             )
+ *             .withSort(QUser.user.name, Order.DESC)
+ *             .fetchPage(1, 10);
+ * }
+ * 
+ */ +public class SqlPaginatedQuery { + + private final SQLQuery sqlQuery; + + private SqlPaginatedQuery(SQLQuery sqlQuery) { + this.sqlQuery = sqlQuery; + } + + public static SqlPaginatedQuery fromQuery(SQLQuery sqlQuery) { + return new SqlPaginatedQuery<>(sqlQuery); + } + + @Nonnull + public > SqlPaginatedQuery withSort( + @Nonnull Expression expression, + @Nonnull Order sortDirection + ) { + return new SqlPaginatedQuery<>( + sqlQuery + .orderBy( + new OrderSpecifier<>( + sortDirection, + expression + ) + ) + ); + } + + /** + * Fetches a page of the SQL query provided + * @param pageNumber the number of the page queried (must be >= 1) + * @param pageSize the size of the page queried (must be >= 1) + * @return the corresponding page + */ + @Nonnull + public Page fetchPage( + int pageNumber, + int pageSize + ) { + QueryResults paginatedQueryResults = this.sqlQuery + .offset(Pages.offset(pageNumber, pageSize)) + .limit(pageSize) + .fetchResults(); + + return new Page<>( + paginatedQueryResults.getResults(), + paginatedQueryResults.getTotal(), + Pages.pageCount(pageSize, paginatedQueryResults.getTotal()), + pageNumber, + Pages.hasMore(pageNumber, pageSize, paginatedQueryResults.getTotal()) + ); + } + + /** + * Fetches a slice of the SQL query provided + * @param pageNumber the number of the page queried (must be >= 1) + * @param pageSize the size of the page queried (must be >= 1) + * @return the corresponding slice + */ + @Nonnull + public Slice fetchSlice( + int pageNumber, + int pageSize + ) { + List slicedQueryResults = this.sqlQuery + .offset(Pages.offset(pageNumber, pageSize)) + .limit(pageSize + 1L) + .fetch(); + + boolean hasMore = slicedQueryResults.size() > pageSize; + + // Trim the results to the required size (if needed) + List items = hasMore ? slicedQueryResults.subList(0, pageSize) : slicedQueryResults; + + return new Slice<>( + items, + hasMore + ); + } +} diff --git a/plume-db-querydsl/src/test/java/com/coreoz/plume/db/querydsl/pagination/SqlPaginatedQueryTest.java b/plume-db-querydsl/src/test/java/com/coreoz/plume/db/querydsl/pagination/SqlPaginatedQueryTest.java new file mode 100644 index 0000000..9f0bfd8 --- /dev/null +++ b/plume-db-querydsl/src/test/java/com/coreoz/plume/db/querydsl/pagination/SqlPaginatedQueryTest.java @@ -0,0 +1,227 @@ +package com.coreoz.plume.db.querydsl.pagination; + +import com.coreoz.plume.db.pagination.Page; +import com.coreoz.plume.db.pagination.Slice; +import com.coreoz.plume.db.querydsl.DbQuerydslTestModule; +import com.coreoz.plume.db.querydsl.db.QUser; +import com.coreoz.plume.db.querydsl.db.User; +import com.coreoz.plume.db.querydsl.db.UserDao; +import com.coreoz.plume.db.querydsl.transaction.TransactionManagerQuerydsl; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.querydsl.core.types.Order; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * file V2__add_users.sql + * 10 users were added + */ +public class SqlPaginatedQueryTest { + static final int USER_COUNT = 100; + + static TransactionManagerQuerydsl transactionManagerQuerydsl; + + static { + Injector injector = Guice.createInjector(new DbQuerydslTestModule()); + transactionManagerQuerydsl = injector.getInstance(TransactionManagerQuerydsl.class); + UserDao userDao = injector.getInstance(UserDao.class); + long userCountToInsert = transactionManagerQuerydsl.selectQuery().select(QUser.user).from(QUser.user).fetchCount(); + if (userCountToInsert > USER_COUNT) { + throw new IllegalStateException("There is already "+ userCountToInsert +" users, which is more than " + USER_COUNT + ", USER_COUNT should be increased"); + } + for (int i = 0; i < (USER_COUNT - userCountToInsert); i++) { + User user = new User(); + user.setName("Page user"); + userDao.save(user); + } + } + + @Test + public void fetch_page_with_correct_pagination_should_paginate_users() { + int pageSize = 10; + Page page = SqlPaginatedQuery + .fromQuery( + transactionManagerQuerydsl.selectQuery() + .select(QUser.user) + .from(QUser.user) + ) + .withSort(QUser.user.name, Order.DESC) + .fetchPage(1, pageSize); + + assertThat(page.pagesCount()).isEqualTo(USER_COUNT / pageSize); + assertThat(page.totalCount()).isEqualTo(USER_COUNT); + assertThat(page.items()).hasSize(pageSize); + assertThat(page.hasMore()).isTrue(); + } + + @Test + public void fetch_page_with_wrong_pagination_should_return_empty_items() { + int pageSize = 10; + Page page = SqlPaginatedQuery + .fromQuery( + transactionManagerQuerydsl.selectQuery() + .select(QUser.user) + .from(QUser.user) + ) + .withSort(QUser.user.name, Order.DESC) + .fetchPage(11, pageSize); + + assertThat(page.pagesCount()).isEqualTo(USER_COUNT / pageSize); + assertThat(page.totalCount()).isEqualTo(USER_COUNT); + assertThat(page.items()).isEmpty(); + assertThat(page.hasMore()).isFalse(); + } + + @Test + public void fetch_page_with_minimum_page_and_page_size_should_return_results() { + Page page = SqlPaginatedQuery + .fromQuery( + transactionManagerQuerydsl.selectQuery() + .select(QUser.user) + .from(QUser.user) + ) + .withSort(QUser.user.name, Order.ASC) + .fetchPage(1, 1); // Minimum page number and page size + + assertThat(page.pagesCount()).isEqualTo(USER_COUNT); + assertThat(page.totalCount()).isEqualTo(USER_COUNT); + assertThat(page.items()).hasSize(1); // Only one item expected due to page size of 1 + assertThat(page.hasMore()).isTrue(); // Has more items since page size is small + } + + @Test + public void fetch_page_with_page_size_larger_than_total_results_should_return_all() { + Page page = SqlPaginatedQuery + .fromQuery( + transactionManagerQuerydsl.selectQuery() + .select(QUser.user) + .from(QUser.user) + ) + .withSort(QUser.user.name, Order.ASC) + .fetchPage(1, USER_COUNT + 1); // Large page size compared to available results + + assertThat(page.pagesCount()).isEqualTo(1); + assertThat(page.totalCount()).isEqualTo(USER_COUNT); + assertThat(page.items()).hasSize(USER_COUNT); // Should return all available users + assertThat(page.hasMore()).isFalse(); // No more items because page size exceeds total results + } + + @Test(expected = IllegalArgumentException.class) + public void fetch_page_with_invalid_negative_page_number_should_throw_exception() { + SqlPaginatedQuery + .fromQuery( + transactionManagerQuerydsl.selectQuery() + .select(QUser.user) + .from(QUser.user) + ) + .withSort(QUser.user.name, Order.ASC) + .fetchPage(-1, 10); // Invalid negative page number + } + + @Test(expected = IllegalArgumentException.class) + public void fetch_page_with_invalid_negative_page_size_should_throw_exception() { + SqlPaginatedQuery + .fromQuery( + transactionManagerQuerydsl.selectQuery() + .select(QUser.user) + .from(QUser.user) + ) + .withSort(QUser.user.name, Order.ASC) + .fetchPage(1, -10); // Invalid negative page size + } + + @Test + public void fetch_page_without_sorting_should_paginate_users() { + int pageSize = 10; + Page page = SqlPaginatedQuery + .fromQuery( + transactionManagerQuerydsl.selectQuery() + .select(QUser.user) + .from(QUser.user) + ) + .fetchPage(1, pageSize); // No sorting applied + + assertThat(page.pagesCount()).isEqualTo(USER_COUNT / pageSize); + assertThat(page.totalCount()).isEqualTo(USER_COUNT); + assertThat(page.items()).hasSize(pageSize); + assertThat(page.hasMore()).isTrue(); + } + + @Test + public void fetch_page_with_empty_query_should_return_no_results() { + Page page = SqlPaginatedQuery + .fromQuery( + transactionManagerQuerydsl.selectQuery() + .select(QUser.user) + .from(QUser.user) + .where(QUser.user.name.eq("Non-existent user")) // Query with no matches + ) + .fetchPage(1, 10); + + assertThat(page.totalCount()).isZero(); // No total count + assertThat(page.items()).isEmpty(); // No items should be returned + assertThat(page.hasMore()).isFalse(); // No more pages since there's no data + } + + @Test + public void fetch_slice_with_minimum_page_and_page_size_should_return_results() { + Slice slice = SqlPaginatedQuery + .fromQuery( + transactionManagerQuerydsl.selectQuery() + .select(QUser.user) + .from(QUser.user) + ) + .withSort(QUser.user.name, Order.ASC) + .fetchSlice(1, 1); // Minimum page number and page size + + assertThat(slice.items()).hasSize(1); // Only one item expected due to page size of 1 + assertThat(slice.hasMore()).isTrue(); // Has more items because of page size being small + } + + @Test + public void fetch_slice_with_empty_query_should_return_no_results() { + Slice slice = SqlPaginatedQuery + .fromQuery( + transactionManagerQuerydsl.selectQuery() + .select(QUser.user) + .from(QUser.user) + .where(QUser.user.name.eq("Non-existent user")) // Query with no matches + ) + .fetchSlice(1, 10); + + assertThat(slice.items()).isEmpty(); // No items should be returned + assertThat(slice.hasMore()).isFalse(); // No more items since there's no data + } + + @Test + public void fetch_slice_with_correct_pagination_should_slice_users() { + Slice page = SqlPaginatedQuery + .fromQuery( + transactionManagerQuerydsl.selectQuery() + .select(QUser.user) + .from(QUser.user) + ) + .withSort(QUser.user.name, Order.ASC) + .fetchSlice(1, USER_COUNT + 1); + + assertThat(page.items()).hasSize(USER_COUNT); + assertThat(page.hasMore()).isFalse(); + } + + @Test + public void fetch_slice_with_wrong_pagination_should_return_empty_items() { + Slice page = SqlPaginatedQuery + .fromQuery( + transactionManagerQuerydsl.selectQuery() + .select(QUser.user) + .from(QUser.user) + ) + .withSort(QUser.user.name, Order.ASC) + .fetchSlice(2, USER_COUNT); + + assertThat(page.items()).isEmpty(); + assertThat(page.hasMore()).isFalse(); + } +} diff --git a/plume-db-querydsl/src/test/resources/db/migration/V1__user_table.sql b/plume-db-querydsl/src/test/resources/db/migration/V1__user_table.sql index ac32f66..749820c 100644 --- a/plume-db-querydsl/src/test/resources/db/migration/V1__user_table.sql +++ b/plume-db-querydsl/src/test/resources/db/migration/V1__user_table.sql @@ -1,5 +1,5 @@ -CREATE TABLE "USER" ( - "ID" BIGINT NOT NULL, +CREATE TABLE "USER" ( + "ID" BIGINT NOT NULL, "NAME" VARCHAR(255), "ACTIVE" BOOLEAN, "CREATION_DATE" TIMESTAMP, diff --git a/plume-db/pom.xml b/plume-db/pom.xml index 695a7c7..9268b48 100644 --- a/plume-db/pom.xml +++ b/plume-db/pom.xml @@ -40,6 +40,13 @@ true
+ + + org.projectlombok + lombok + provided + + com.coreoz diff --git a/plume-db/src/main/java/com/coreoz/plume/db/pagination/Page.java b/plume-db/src/main/java/com/coreoz/plume/db/pagination/Page.java new file mode 100644 index 0000000..bb897b6 --- /dev/null +++ b/plume-db/src/main/java/com/coreoz/plume/db/pagination/Page.java @@ -0,0 +1,49 @@ +package com.coreoz.plume.db.pagination; + +import javax.annotation.Nonnull; +import java.util.List; +import java.util.function.Function; + +/** + * Represents a page of data in a paginated structure. + * + * @param items the items in the page + * @param totalCount the total number of items in the collection + * @param pagesCount the total number of pages + * @param currentPage the current page, starts at 1 + * @param hasMore boolean set to true if there is another page after this one + * + * @param The type of items contained in the page. + */ +public record Page( + @Nonnull List items, + long totalCount, + long pagesCount, + long currentPage, + boolean hasMore +) implements Sliceable { + + /** + * Maps the items of the paginated list + * @param mapper the mapper + * @return the page with mapped items + * @param The type of elements to be mapped to in the page. + *
+ * Usage example: + *
+     * public Page fetchUsers() {
+     *   return this.userService.fetchPage(1, 10).map(user -> new UserUpdated(user));
+     * }
+     * 
+ */ + public Page map(@Nonnull Function mapper) { + return new Page<>( + this.items.stream().map(mapper).toList(), + this.totalCount, + this.pagesCount, + this.currentPage, + this.hasMore + ); + } + +} diff --git a/plume-db/src/main/java/com/coreoz/plume/db/pagination/Pages.java b/plume-db/src/main/java/com/coreoz/plume/db/pagination/Pages.java new file mode 100644 index 0000000..743b186 --- /dev/null +++ b/plume-db/src/main/java/com/coreoz/plume/db/pagination/Pages.java @@ -0,0 +1,20 @@ +package com.coreoz.plume.db.pagination; + +public class Pages { + + private Pages() { + // hide implicit constructor + } + + public static long offset(int pageNumber, int pageSize) { + return (long) (pageNumber - 1) * pageSize; + } + + public static long pageCount(int pageSize, long resultNumber) { + return (resultNumber + pageSize - 1) / pageSize; + } + + public static boolean hasMore(int pageNumber, int pageSize, long resultNumber) { + return pageNumber < pageCount(pageSize, resultNumber); + } +} diff --git a/plume-db/src/main/java/com/coreoz/plume/db/pagination/Paginable.java b/plume-db/src/main/java/com/coreoz/plume/db/pagination/Paginable.java deleted file mode 100644 index 9787437..0000000 --- a/plume-db/src/main/java/com/coreoz/plume/db/pagination/Paginable.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.coreoz.plume.db.pagination; - -import java.util.List; - -/** - * Describe an object that handles pagination. - * - * @param The paginable element - */ -public interface Paginable { - - /** - * Returns the number of elements available - */ - long count(); - - /** - * Returns all the available elements list. - */ - List fetch(); - - /** - * Fetch a page of elements. - * - * @param page The page sought, starts at 0 - * @param pageSize The number of elements by page - * @return the elements list on the page sought - * @throws IndexOutOfBoundsException If page < 0 or if page*pageSize > {@link #count()} - */ - List fetch(int page, int pageSize); - -} diff --git a/plume-db/src/main/java/com/coreoz/plume/db/pagination/Slice.java b/plume-db/src/main/java/com/coreoz/plume/db/pagination/Slice.java new file mode 100644 index 0000000..56bf93b --- /dev/null +++ b/plume-db/src/main/java/com/coreoz/plume/db/pagination/Slice.java @@ -0,0 +1,39 @@ +package com.coreoz.plume.db.pagination; + +import javax.annotation.Nonnull; +import java.util.List; +import java.util.function.Function; + +/** + * Represents a portion (or slice) of a larger dataset. + * + * @param items the items in the slice + * @param hasMore boolean set to true if there is another slice after this one + * + * @param The type of elements contained in the slice. + */ +public record Slice( + @Nonnull List items, + boolean hasMore +) implements Sliceable { + + /** + * Maps the items of the slice + * @param mapper the mapper + * @return the slice with mapped items + * @param The type of elements to be mapped to in the slice. + *
+ * Usage example: + *
+     * public Page fetchUsers() {
+     *   return this.userService.fetchSlice(1, 10).map(user -> new UserUpdated(user));
+     * }
+     * 
+ */ + public Slice map(@Nonnull Function mapper) { + return new Slice<>( + this.items.stream().map(mapper).toList(), + this.hasMore + ); + } +} diff --git a/plume-db/src/main/java/com/coreoz/plume/db/pagination/Sliceable.java b/plume-db/src/main/java/com/coreoz/plume/db/pagination/Sliceable.java new file mode 100644 index 0000000..da24320 --- /dev/null +++ b/plume-db/src/main/java/com/coreoz/plume/db/pagination/Sliceable.java @@ -0,0 +1,24 @@ +package com.coreoz.plume.db.pagination; + +import java.util.List; + +/** + * Common interface for {@link Page} or {@link Slice} data structures. + * + * @param The type of elements contained in the pageable structure. + */ +public interface Sliceable { + /** + * Returns the list of elements contained in this sliceable structure. + * + * @return A list of elements. + */ + List items(); + + /** + * Indicates whether there are more elements available beyond this sliceable structure. + * + * @return {@code true} if there are more elements, {@code false} otherwise. + */ + boolean hasMore(); +} diff --git a/plume-db/src/test/java/com/coreoz/plume/db/pagination/PageTest.java b/plume-db/src/test/java/com/coreoz/plume/db/pagination/PageTest.java new file mode 100644 index 0000000..eb690ea --- /dev/null +++ b/plume-db/src/test/java/com/coreoz/plume/db/pagination/PageTest.java @@ -0,0 +1,29 @@ +package com.coreoz.plume.db.pagination; + +import org.assertj.core.api.Assertions; +import org.junit.Test; + +import java.util.List; + +public class PageTest { + + @Test + public void should_map_users() { + User user = new User(1L, "To fetch"); + Page page = new Page<>( + List.of(user), + 1, + 1, + 1, + true + ); + + Page userNames = page.map(User::name); + + Assertions.assertThat(userNames.items().stream().findFirst()).isNotEmpty(); + Assertions.assertThat(userNames.items().stream().findFirst().get()).contains("To fetch"); + } + + private record User(long id, String name) { + } +} diff --git a/plume-db/src/test/java/com/coreoz/plume/db/pagination/PagesTest.java b/plume-db/src/test/java/com/coreoz/plume/db/pagination/PagesTest.java new file mode 100644 index 0000000..2f2d85a --- /dev/null +++ b/plume-db/src/test/java/com/coreoz/plume/db/pagination/PagesTest.java @@ -0,0 +1,38 @@ +package com.coreoz.plume.db.pagination; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class PagesTest { + + @Test + public void pageable_should_return_corresponding_offset() { + assertThat(Pages.offset(2, 10)).isEqualTo(10); + } + + @Test + public void pageable_with_page_1_should_return_corresponding_offset() { + assertThat(Pages.offset(1, 150)).isZero(); + } + + @Test + public void pageable_should_return_corresponding_page_count() { + assertThat(Pages.pageCount(150, 475)).isEqualTo(4); + } + + @Test + public void pageable_with_no_results_should_return_corresponding_page_count() { + assertThat(Pages.pageCount(150, 0)).isZero(); + } + + @Test + public void pageable_should_return_corresponding_has_more() { + assertThat(Pages.hasMore(3, 150, 450)).isFalse(); + } + + @Test + public void pageable_with_0_results_should_return_corresponding_has_more() { + assertThat(Pages.hasMore(1, 150, 0)).isFalse(); + } +} diff --git a/plume-db/src/test/java/com/coreoz/plume/db/pagination/SliceTest.java b/plume-db/src/test/java/com/coreoz/plume/db/pagination/SliceTest.java new file mode 100644 index 0000000..681eb14 --- /dev/null +++ b/plume-db/src/test/java/com/coreoz/plume/db/pagination/SliceTest.java @@ -0,0 +1,26 @@ +package com.coreoz.plume.db.pagination; + +import org.assertj.core.api.Assertions; +import org.junit.Test; + +import java.util.List; + +public class SliceTest { + + @Test + public void should_map_users() { + User user = new User(1L, "To fetch"); + Slice page = new Slice<>( + List.of(user), + true + ); + + Slice userNames = page.map(User::name); + + Assertions.assertThat(userNames.items().stream().findFirst()).isNotEmpty(); + Assertions.assertThat(userNames.items().stream().findFirst().get()).contains("To fetch"); + } + + private record User(long id, String name) { + } +}