From 68bc52128d0681fc14a871a0e77c8e43a215c188 Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Mon, 15 Apr 2024 10:55:47 -0400 Subject: [PATCH 01/20] Initial addition of cursored pagination - Create the CursoredPageable type. - Modify the DefaultSqlPreparedQuery to support cursored pageable for SQL. - Modify DefaultFindPageInterceptor to return correct pageable for further pagination in cursored case. --- .../jdbc/h2/H2CursoredPaginationSpec.groovy | 50 ++++ .../PostgresCursoredPaginationSpec.groovy | 42 +++ .../SqlServerCursoredPaginationSpec.groovy | 43 +++ .../data/model/CursoredPageable.java | 135 +++++++++ .../data/model/DefaultCursoredPageable.java | 159 +++++++++++ .../io/micronaut/data/model/DefaultSort.java | 5 + .../java/io/micronaut/data/model/Sort.java | 8 + .../builder/AbstractSqlLikeQueryBuilder.java | 132 +++++---- .../query/builder/sql/SqlQueryBuilder.java | 3 +- .../intercept/DefaultFindPageInterceptor.java | 12 +- .../internal/sql/DefaultSqlPreparedQuery.java | 201 ++++++++++++++ .../tck/tests/AbstractCursoredPageSpec.groovy | 256 ++++++++++++++++++ 12 files changed, 989 insertions(+), 57 deletions(-) create mode 100644 data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/H2CursoredPaginationSpec.groovy create mode 100644 data-jdbc/src/test/groovy/io/micronaut/data/jdbc/postgres/PostgresCursoredPaginationSpec.groovy create mode 100644 data-jdbc/src/test/groovy/io/micronaut/data/jdbc/sqlserver/SqlServerCursoredPaginationSpec.groovy create mode 100644 data-model/src/main/java/io/micronaut/data/model/CursoredPageable.java create mode 100644 data-model/src/main/java/io/micronaut/data/model/DefaultCursoredPageable.java create mode 100644 data-tck/src/main/groovy/io/micronaut/data/tck/tests/AbstractCursoredPageSpec.groovy diff --git a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/H2CursoredPaginationSpec.groovy b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/H2CursoredPaginationSpec.groovy new file mode 100644 index 00000000000..a27d646621f --- /dev/null +++ b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/H2CursoredPaginationSpec.groovy @@ -0,0 +1,50 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.data.jdbc.h2 + +import io.micronaut.data.tck.repositories.BookRepository +import io.micronaut.data.tck.repositories.PersonRepository +import io.micronaut.data.tck.tests.AbstractCursoredPageSpec +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import jakarta.inject.Inject +import spock.lang.Shared + +@MicronautTest +@H2DBProperties +class H2CursoredPaginationSpec extends AbstractCursoredPageSpec { + @Inject + @Shared + H2PersonRepository pr + + @Inject + @Shared + H2BookRepository br + + @Override + PersonRepository getPersonRepository() { + return pr + } + + @Override + BookRepository getBookRepository() { + return br + } + + @Override + void init() { + pr.deleteAll() + } +} diff --git a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/postgres/PostgresCursoredPaginationSpec.groovy b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/postgres/PostgresCursoredPaginationSpec.groovy new file mode 100644 index 00000000000..9020aeae5bc --- /dev/null +++ b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/postgres/PostgresCursoredPaginationSpec.groovy @@ -0,0 +1,42 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.data.jdbc.postgres + +import io.micronaut.context.ApplicationContext +import io.micronaut.data.tck.repositories.BookRepository +import io.micronaut.data.tck.repositories.PersonRepository +import io.micronaut.data.tck.tests.AbstractCursoredPageSpec +import spock.lang.AutoCleanup +import spock.lang.Shared + +class PostgresCursoredPaginationSpec extends AbstractCursoredPageSpec implements PostgresTestPropertyProvider { + @Shared @AutoCleanup ApplicationContext context + + @Override + PersonRepository getPersonRepository() { + return context.getBean(PostgresPersonRepository) + } + + @Override + BookRepository getBookRepository() { + return context.getBean(PostgresBookRepository) + } + + @Override + void init() { + context = ApplicationContext.run(getProperties()) + } +} diff --git a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/sqlserver/SqlServerCursoredPaginationSpec.groovy b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/sqlserver/SqlServerCursoredPaginationSpec.groovy new file mode 100644 index 00000000000..dc13c07c9cb --- /dev/null +++ b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/sqlserver/SqlServerCursoredPaginationSpec.groovy @@ -0,0 +1,43 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.data.jdbc.sqlserver + +import io.micronaut.context.ApplicationContext +import io.micronaut.data.tck.repositories.BookRepository +import io.micronaut.data.tck.repositories.PersonRepository +import io.micronaut.data.tck.tests.AbstractCursoredPageSpec +import spock.lang.AutoCleanup +import spock.lang.Shared + +class SqlServerCursoredPaginationSpec extends AbstractCursoredPageSpec implements MSSQLTestPropertyProvider { + + @Shared @AutoCleanup ApplicationContext context + + @Override + PersonRepository getPersonRepository() { + return context.getBean(MSSQLPersonRepository) + } + + @Override + BookRepository getBookRepository() { + return context.getBean(MSBookRepository) + } + + @Override + void init() { + context = ApplicationContext.run(properties) + } +} diff --git a/data-model/src/main/java/io/micronaut/data/model/CursoredPageable.java b/data-model/src/main/java/io/micronaut/data/model/CursoredPageable.java new file mode 100644 index 00000000000..e89e3948b63 --- /dev/null +++ b/data-model/src/main/java/io/micronaut/data/model/CursoredPageable.java @@ -0,0 +1,135 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.data.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.micronaut.core.annotation.Introspected; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.serde.annotation.Serdeable; + +import java.util.List; + +/** + * Models pageable data that uses a cursor. + * + * @author Andriy Dmytruk + * @since 4.6.1 + */ +@Serdeable +@Introspected +@JsonIgnoreProperties(ignoreUnknown = true) +public interface CursoredPageable extends Pageable { + + /** + * Constant for no pagination. + */ + CursoredPageable UNPAGED = new DefaultCursoredPageable( + 0, null, null, false, -1, null + ); + + /** + * @return The cursor values corresponding to the beginning of queried data. + * This cursor is used for forward pagination. + */ + @Nullable + List getStartCursor(); + + /** + * @return The cursor values corresponding to the end of queried data. + * This cursor is used for backward pagination. + */ + @Nullable + List getEndCursor(); + + /** + * Whether the pageable is traversing backwards. + * + * @return Whether cursor is going in reverse direction. + */ + boolean isBackward(); + + @Override + default @NonNull CursoredPageable next() { + throw new IllegalStateException("To get the next CursoredPageable, you must retrieve this one from a page"); + } + + @Override + default @NonNull CursoredPageable previous() { + throw new IllegalStateException("To get the next CursoredPageable, you must retrieve this one from a page"); + } + + /** + * Creates a new {@link CursoredPageable} with the given sort. + * + * @param sort The sort + * @return The pageable + */ + static @NonNull CursoredPageable from(Sort sort) { + if (sort == null) { + return UNPAGED; + } + return new DefaultCursoredPageable( + 0, null, null, false, -1, sort + ); + } + + /** + * Creates a new {@link CursoredPageable} with the given sort and page size. + * + * @param size The page size + * @param sort The sort + * @return The pageable + */ + static @NonNull CursoredPageable from( + @JsonProperty("size") int size, + @JsonProperty("sort") @Nullable Sort sort + ) { + return new DefaultCursoredPageable(0, null, null, false, size, sort); + } + + /** + * Creates a new {@link CursoredPageable} with the given cursor. + * + * @param page The page + * @param startCursor The cursor pointing to the beginning of the traversed data. + * @param endCursor The cursor pointing to the end the traversed data. + * @param isBackward Whether the cursor is for backward traversing + * @param size The page size + * @param sort The sort + * @return The pageable + */ + @JsonCreator + static @NonNull CursoredPageable from( + @JsonProperty("number") int page, + @JsonProperty("startCursor") @Nullable List startCursor, + @JsonProperty("endCursor") @Nullable List endCursor, + @JsonProperty(value = "isBackward", defaultValue = "false") boolean isBackward, + @JsonProperty("size") int size, + @JsonProperty("sort") @Nullable Sort sort + ) { + return new DefaultCursoredPageable(page, startCursor, endCursor, isBackward, size, sort); + } + + /** + * @return A new instance without paging data. + */ + static @NonNull CursoredPageable unpaged() { + return UNPAGED; + } +} diff --git a/data-model/src/main/java/io/micronaut/data/model/DefaultCursoredPageable.java b/data-model/src/main/java/io/micronaut/data/model/DefaultCursoredPageable.java new file mode 100644 index 00000000000..f05dcbff6f5 --- /dev/null +++ b/data-model/src/main/java/io/micronaut/data/model/DefaultCursoredPageable.java @@ -0,0 +1,159 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.data.model; + +import io.micronaut.core.annotation.Creator; +import io.micronaut.core.annotation.Introspected; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; + +import java.util.List; +import java.util.Objects; + +/** + * The default cursored pageable implementation. + * + * @author Andriy Dmytruk + * @since 4.6.1 + */ +@Introspected +final class DefaultCursoredPageable implements CursoredPageable { + + private final int max; + private final int number; + private final List startCursor; + private final List endCursor; + private final boolean isBackward; + private final Sort sort; + + /** + * Default constructor. + * + * @param page The page. + * @param startCursor The cursor that is pointing to the start of the data. + * This cursor will be used for forward pagination. + * @param endCursor The cursor that is pointing to the end of the data. + * This cursor will be used for backward pagination + * @param isBackward Whether user requested for backward pagination. + * @param size The size of a page + * @param sort The sorting + */ + @Creator + DefaultCursoredPageable(int page, @Nullable List startCursor, @Nullable List endCursor, boolean isBackward, int size, @Nullable Sort sort) { + if (page < 0) { + throw new IllegalArgumentException("Page index cannot be negative"); + } + if (size == 0) { + throw new IllegalArgumentException("Size cannot be 0"); + } + this.max = size; + this.number = page; + this.startCursor = startCursor; + this.endCursor = endCursor; + this.isBackward = isBackward; + this.sort = sort == null ? Sort.unsorted() : sort; + } + + @Override + public int getSize() { + return max; + } + + @Override + public int getNumber() { + return number; + } + + @NonNull + @Override + public Sort getSort() { + return sort; + } + + @Override + public List getStartCursor() { + return startCursor; + } + + @Override + public List getEndCursor() { + return endCursor; + } + + @Override + public boolean isBackward() { + return isBackward; + } + + @Override + public CursoredPageable next() { + if (endCursor != null) { + return new DefaultCursoredPageable( + number + 1, + endCursor, + null, + false, + getSize(), + getSort() + ); + } + return CursoredPageable.super.next(); + } + + @Override + public CursoredPageable previous() { + if (startCursor != null) { + return new DefaultCursoredPageable( + Math.max(number - 1, 0), + null, + startCursor, + true, + getSize(), + getSort() + ); + } + return CursoredPageable.super.previous(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof DefaultCursoredPageable that)) { + return false; + } + return max == that.max && + Objects.equals(startCursor, that.startCursor) && + Objects.equals(sort, that.sort); + } + + @Override + public int hashCode() { + return Objects.hash(max, startCursor, sort); + } + + @Override + public String toString() { + return "DefaultCursoredPageable{" + + "max=" + max + + ", number=" + number + + ", startCursor=" + startCursor + + ", endCursor=" + endCursor + + ", sort=" + sort + + '}'; + } +} diff --git a/data-model/src/main/java/io/micronaut/data/model/DefaultSort.java b/data-model/src/main/java/io/micronaut/data/model/DefaultSort.java index 063cc9370c7..2f685a30f7d 100644 --- a/data-model/src/main/java/io/micronaut/data/model/DefaultSort.java +++ b/data-model/src/main/java/io/micronaut/data/model/DefaultSort.java @@ -96,6 +96,11 @@ public DefaultSort order(@NonNull String propertyName, @NonNull Order.Direction return order(new Order(propertyName, direction, false)); } + @Override + public String toString() { + return "DefaultSort{orderBy=" + orderBy + '}'; + } + @Override public boolean equals(Object o) { if (this == o) { diff --git a/data-model/src/main/java/io/micronaut/data/model/Sort.java b/data-model/src/main/java/io/micronaut/data/model/Sort.java index 9a4fa3870d1..b209dacfa3c 100644 --- a/data-model/src/main/java/io/micronaut/data/model/Sort.java +++ b/data-model/src/main/java/io/micronaut/data/model/Sort.java @@ -234,6 +234,14 @@ public enum Direction { ASC, DESC } + @Override + public String toString() { + return "SORT{" + property + + (direction == Direction.ASC ? ", ASC" : ", DESC") + + (ignoreCase ? ", ignoreCase" : "") + + ")"; + } + @Override public boolean equals(Object o) { if (this == o) { diff --git a/data-model/src/main/java/io/micronaut/data/model/query/builder/AbstractSqlLikeQueryBuilder.java b/data-model/src/main/java/io/micronaut/data/model/query/builder/AbstractSqlLikeQueryBuilder.java index 8492ab4bd6b..74eeffe340f 100644 --- a/data-model/src/main/java/io/micronaut/data/model/query/builder/AbstractSqlLikeQueryBuilder.java +++ b/data-model/src/main/java/io/micronaut/data/model/query/builder/AbstractSqlLikeQueryBuilder.java @@ -1969,8 +1969,6 @@ public QueryResult buildOrderBy(String query, @NonNull PersistentEntity entity, StringBuilder buff = new StringBuilder(ORDER_BY_CLAUSE); Iterator i = orders.iterator(); - String jsonEntityColumn = getJsonEntityColumn(annotationMetadata); - while (i.hasNext()) { Sort.Order order = i.next(); String property = order.getProperty(); @@ -1978,59 +1976,9 @@ public QueryResult buildOrderBy(String query, @NonNull PersistentEntity entity, if (ignoreCase) { buff.append("LOWER("); } - if (nativeQuery) { - buff.append(property); - } else { - PersistentPropertyPath path = entity.getPropertyPath(property); - if (path == null) { - throw new IllegalArgumentException("Cannot sort on non-existent property path: " + property); - } - List associations = new ArrayList<>(path.getAssociations()); - int assocCount = associations.size(); - // If last association is embedded, it does not need to be joined to the alias since it will be in the destination table - // JPA/Hibernate is special case and in that case we leave association for specific handling below - if (assocCount > 0 && computePropertyPaths() && associations.get(assocCount - 1) instanceof Embedded) { - associations.remove(assocCount - 1); - } - if (associations.isEmpty()) { - buff.append(getAliasName(entity)); - } else { - StringJoiner joiner = new StringJoiner("."); - for (Association association : associations) { - joiner.add(association.getName()); - } - String joinAlias = getAliasName(new JoinPath(joiner.toString(), associations.toArray(new Association[0]), Join.Type.DEFAULT, null)); - if (!computePropertyPaths()) { - if (!query.contains(" " + joinAlias + " ") && !query.endsWith(" " + joinAlias)) { - // Special hack case for JPA, Hibernate can join the relation with cross join automatically when referenced by the property path - // This probably should be removed in the future major version - buff.append(getAliasName(entity)).append(DOT); - StringJoiner pathJoiner = new StringJoiner("."); - for (Association association : associations) { - pathJoiner.add(association.getName()); - } - buff.append(pathJoiner); - } else { - buff.append(joinAlias); - } - } else { - buff.append(joinAlias); - } - } - buff.append(DOT); - - if (jsonEntityColumn != null) { - buff.append(jsonEntityColumn).append(DOT); - } - - if (!computePropertyPaths() || jsonEntityColumn != null) { - buff.append(path.getProperty().getName()); - } else { - buff.append(getColumnName(path.getProperty())); - } - if (ignoreCase) { - buff.append(")"); - } + buff.append(buildPropertyByName(property, query, entity, annotationMetadata, nativeQuery)); + if (ignoreCase) { + buff.append(")"); } buff.append(SPACE).append(order.getDirection()); if (i.hasNext()) { @@ -2046,6 +1994,80 @@ public QueryResult buildOrderBy(String query, @NonNull PersistentEntity entity, ); } + /** + * Encode the given property retrieval into a query instance. + * For example, property name might be encoded as {@code `person_.name`} using + * its path and table's alias. + * + * @param query The query + * @param entity The root entity + * @param annotationMetadata The annotation metadata + * @param nativeQuery Whether the query is native query, in which case the property name will be supplied by the user and not verified + * @return The encoded query + */ + public String buildPropertyByName( + @NonNull String propertyName, @NonNull String query, + @NonNull PersistentEntity entity, @NonNull AnnotationMetadata annotationMetadata, + boolean nativeQuery + ) { + if (nativeQuery) { + return propertyName; + } + + PersistentPropertyPath path = entity.getPropertyPath(propertyName); + if (path == null) { + throw new IllegalArgumentException("Cannot sort on non-existent property path: " + propertyName); + } + List associations = new ArrayList<>(path.getAssociations()); + int assocCount = associations.size(); + // If last association is embedded, it does not need to be joined to the alias since it will be in the destination table + // JPA/Hibernate is special case and in that case we leave association for specific handling below + if (assocCount > 0 && computePropertyPaths() && associations.get(assocCount - 1) instanceof Embedded) { + associations.remove(assocCount - 1); + } + + StringBuilder buff = new StringBuilder(); + if (associations.isEmpty()) { + buff.append(getAliasName(entity)); + } else { + StringJoiner joiner = new StringJoiner("."); + for (Association association : associations) { + joiner.add(association.getName()); + } + String joinAlias = getAliasName(new JoinPath(joiner.toString(), associations.toArray(new Association[0]), Join.Type.DEFAULT, null)); + if (!computePropertyPaths()) { + if (!query.contains(" " + joinAlias + " ") && !query.endsWith(" " + joinAlias)) { + // Special hack case for JPA, Hibernate can join the relation with cross join automatically when referenced by the property path + // This probably should be removed in the future major version + buff.append(getAliasName(entity)).append(DOT); + StringJoiner pathJoiner = new StringJoiner("."); + for (Association association : associations) { + pathJoiner.add(association.getName()); + } + buff.append(pathJoiner); + } else { + buff.append(joinAlias); + } + } else { + buff.append(joinAlias); + } + } + buff.append(DOT); + + String jsonEntityColumn = getJsonEntityColumn(annotationMetadata); + if (jsonEntityColumn != null) { + buff.append(jsonEntityColumn).append(DOT); + } + + if (!computePropertyPaths() || jsonEntityColumn != null) { + buff.append(path.getProperty().getName()); + } else { + buff.append(getColumnName(path.getProperty())); + } + + return buff.toString(); + } + /** * Join associations and property as path. * diff --git a/data-model/src/main/java/io/micronaut/data/model/query/builder/sql/SqlQueryBuilder.java b/data-model/src/main/java/io/micronaut/data/model/query/builder/sql/SqlQueryBuilder.java index 38fc7203bc4..84b769fb71c 100644 --- a/data-model/src/main/java/io/micronaut/data/model/query/builder/sql/SqlQueryBuilder.java +++ b/data-model/src/main/java/io/micronaut/data/model/query/builder/sql/SqlQueryBuilder.java @@ -39,6 +39,7 @@ import io.micronaut.data.annotation.sql.SqlMembers; import io.micronaut.data.exceptions.MappingException; import io.micronaut.data.model.Association; +import io.micronaut.data.model.CursoredPageable; import io.micronaut.data.model.DataType; import io.micronaut.data.model.Embedded; import io.micronaut.data.model.JsonDataType; @@ -1213,7 +1214,7 @@ public QueryResult buildPagination(@NonNull Pageable pageable) { int size = pageable.getSize(); if (size > 0) { StringBuilder builder = new StringBuilder(" "); - long from = pageable.getOffset(); + long from = pageable instanceof CursoredPageable ? 0 : pageable.getOffset(); switch (dialect) { case H2: case MYSQL: diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/DefaultFindPageInterceptor.java b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/DefaultFindPageInterceptor.java index 6aa7706bfa8..367b1103e63 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/DefaultFindPageInterceptor.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/DefaultFindPageInterceptor.java @@ -21,10 +21,14 @@ import io.micronaut.data.annotation.Query; import io.micronaut.data.intercept.FindPageInterceptor; import io.micronaut.data.intercept.RepositoryMethodKey; +import io.micronaut.data.model.CursoredPageable; import io.micronaut.data.model.Page; +import io.micronaut.data.model.Pageable; import io.micronaut.data.model.runtime.PreparedQuery; import io.micronaut.data.operations.RepositoryOperations; +import io.micronaut.data.runtime.operations.internal.sql.DefaultSqlPreparedQuery; +import java.util.Collections; import java.util.List; /** @@ -56,7 +60,13 @@ public R intercept(RepositoryMethodKey methodKey, MethodInvocationContext List resultList = (List) CollectionUtils.iterableToList(iterable); Number n = operations.findOne(countQuery); Long result = n != null ? n.longValue() : 0; - Page page = Page.of(resultList, getPageable(context), result); + + Pageable pageable = getPageable(context); + if (preparedQuery instanceof DefaultSqlPreparedQuery sqlPreparedQuery) { + pageable = sqlPreparedQuery.updatePageable(resultList, pageable, result); + } + + Page page = Page.of(resultList, pageable, result); if (returnType.isInstance(page)) { return (R) page; } else { diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/DefaultSqlPreparedQuery.java b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/DefaultSqlPreparedQuery.java index 64d0d0aae5c..fe1333df7b0 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/DefaultSqlPreparedQuery.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/DefaultSqlPreparedQuery.java @@ -17,9 +17,15 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; import io.micronaut.data.exceptions.DataAccessException; +import io.micronaut.data.model.CursoredPageable; +import io.micronaut.data.model.DataType; import io.micronaut.data.model.Pageable; +import io.micronaut.data.model.PersistentProperty; import io.micronaut.data.model.Sort; +import io.micronaut.data.model.Sort.Order; +import io.micronaut.data.model.Sort.Order.Direction; import io.micronaut.data.model.query.builder.AbstractSqlLikeQueryBuilder; import io.micronaut.data.model.query.builder.sql.Dialect; import io.micronaut.data.model.query.builder.sql.SqlQueryBuilder; @@ -34,7 +40,10 @@ import io.micronaut.data.runtime.query.internal.DelegateStoredQuery; import java.lang.reflect.Array; +import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; +import java.util.List; import java.util.Map; /** @@ -48,6 +57,8 @@ @Internal public class DefaultSqlPreparedQuery extends DefaultBindableParametersPreparedQuery implements SqlPreparedQuery, DelegatePreparedQuery { + protected List cursorQueryBindings; + protected List> cursorProperties; protected final SqlStoredQuery sqlStoredQuery; protected String query; @@ -157,6 +168,24 @@ public void attachPageable(Pageable pageable, boolean isSingleResult) { SqlQueryBuilder queryBuilder = sqlStoredQuery.getQueryBuilder(); StringBuilder added = new StringBuilder(); Sort sort = pageable.getSort(); + if (pageable instanceof CursoredPageable cursored) { + // Create a sort for the cursored pagination. The sort must produce a unique + // sorting on the rows. Therefore, we make sure id is present in it. + List orders = new ArrayList<>(sort.getOrderBy()); + for (PersistentProperty idProperty: persistentEntity.getIdentityProperties()) { + String name = idProperty.getName(); + if (orders.stream().noneMatch(o -> o.getProperty().equals(name))) { + orders.add(Order.asc(name)); + } + } + sort = Sort.of(orders); + if (cursored.isBackward()) { + sort = reverseSort(sort); + } + added.append(buildCursorPagination( + cursored.isBackward() ? cursored.getEndCursor() : cursored.getStartCursor(), sort + )); + } if (sort.isSorted()) { added.append(queryBuilder.buildOrderBy("", persistentEntity, sqlStoredQuery.getAnnotationMetadata(), sort, isNative()).getQuery()); } else if (isSqlServerWithoutOrderBy(query, sqlStoredQuery.getDialect())) { @@ -180,6 +209,150 @@ public void attachPageable(Pageable pageable, boolean isSingleResult) { } } + /** + * A utility method for reversing the sort. + * + * @param sort The current sort + * @return reversed sort + */ + private Sort reverseSort(Sort sort) { + if (!sort.isSorted()) { + return sort; + } + List orders = new ArrayList<>(); + for (Order order : sort.getOrderBy()) { + orders.add(new Order( + order.getProperty(), + order.getDirection() == Direction.ASC ? Direction.DESC : Direction.ASC, + order.isIgnoreCase() + )); + } + return Sort.of(orders); + } + + /** + * Add relevant query clauses and query bindings to use cursored pagination. + * + * @param cursor The supplied cursor + * @param sort The sorting that will be used in the query + * @return The additional query part + */ + @NonNull + private String buildCursorPagination(@Nullable List cursor, @NonNull Sort sort) { + List orders = sort.getOrderBy(); + cursorProperties = new ArrayList<>(); + for (Order order: orders) { + cursorProperties.add(getPersistentEntity().getPropertyByName(order.getProperty())); + } + if (cursor == null) + return ""; + if (orders.size() != cursor.size()) { + throw new IllegalArgumentException("The cursor must match the sorting size"); + } + if (orders.isEmpty()) { + throw new IllegalArgumentException("At least one sorting property must be supplied"); + } + + List cursorBindings = new ArrayList<>(); + cursorQueryBindings = new ArrayList<>(); + for (int i = 0; i < cursor.size(); ++i) { + cursorBindings.add(new CursoredQueryParameterBinder( + "cursor_" + i, cursorProperties.get(i).getDataType(), cursor.get(i) + )); + } + + StringBuilder builder = new StringBuilder(" "); + if (query.contains("WHERE")) { + int i = query.indexOf("WHERE") + "WHERE".length(); + query = query.substring(0, i) + "(" + query.substring(i) + ")"; + builder.append(" AND ("); + } else { + builder.append("WHERE "); + } + for (int i = 0; i < orders.size(); ++i) { + builder.append("("); + for (int j = 0; j <= i; ++j) { + String propertyName = orders.get(j).getProperty(); + builder.append(sqlStoredQuery.getQueryBuilder().buildPropertyByName(propertyName, query, getPersistentEntity(), getAnnotationMetadata(), isNative())); + if (orders.get(i).isAscending()) { + builder.append(i == j ? " > " : " = "); + } else { + builder.append(i == j ? " < " : " = "); + } + cursorQueryBindings.add(cursorBindings.get(j)); + builder.append("?"); + if (i != j) { + builder.append(" AND "); + } + } + builder.append(")"); + if (i < orders.size() - 1) { + builder.append(" OR "); + } + } + + if (query.contains("WHERE")) { + builder.append(")"); + } + return builder.toString(); + } + + /** + * Modify pageable based on the scan results. + * This is required for cursored pageable, as cursor is created from the results. + * + * @param results The scanning results + * @param pageable The pageable sent by user + * @return The updated pageable + */ + public Pageable updatePageable(List results, Pageable pageable, long totalSize) { + if (pageable instanceof CursoredPageable cursored) { + if (cursored.isBackward()) { + Collections.reverse(results); + } + + List startCursor = null; + List endCursor = null; + if (!results.isEmpty()) { + if (!cursored.isBackward() || results.size() == cursored.getSize()) { + E firstValue = (E) results.get(0); + startCursor = new ArrayList<>(cursorProperties.size()); + for (RuntimePersistentProperty property : cursorProperties) { + startCursor.add(property.getProperty().get(firstValue)); + } + } + if (cursored.isBackward() || results.size() == cursored.getSize()) { + E lastValue = (E) results.get(results.size() - 1); + endCursor = new ArrayList<>(cursorProperties.size()); + for (RuntimePersistentProperty property : cursorProperties) { + endCursor.add(property.getProperty().get(lastValue)); + } + } + } else { + if (cursored.isBackward()) { + endCursor = cursored.getEndCursor(); + } else { + startCursor = cursored.getStartCursor(); + } + } + return CursoredPageable.from( + cursored.getNumber(), startCursor, endCursor, cursored.isBackward(), cursored.getSize(), + cursored.getSort() + ); + } + return pageable; + } + + @Override + public void bindParameters(Binder binder, E entity, Map previousValues) { + super.bindParameters(binder, entity, previousValues); + if (cursorQueryBindings != null) { + for (QueryParameterBinding queryParameterBinding : cursorQueryBindings) { + binder.bindOne(queryParameterBinding, queryParameterBinding.getValue()); + } + } + } + @Override public QueryResultInfo getQueryResultInfo() { return sqlStoredQuery.getQueryResultInfo(); @@ -237,4 +410,32 @@ protected int sizeOf(Object value) { } return 1; } + + protected static class CursoredQueryParameterBinder implements QueryParameterBinding { + + private final String name; + private DataType dataType; + private final Object value; + + public CursoredQueryParameterBinder(String name, DataType dataType, Object value) { + this.name = name; + this.dataType = dataType; + this.value = value; + } + + @Override + public String getName() { + return name; + } + + @Override + public DataType getDataType() { + return dataType; + } + + @Override + public Object getValue() { + return value; + } + } } diff --git a/data-tck/src/main/groovy/io/micronaut/data/tck/tests/AbstractCursoredPageSpec.groovy b/data-tck/src/main/groovy/io/micronaut/data/tck/tests/AbstractCursoredPageSpec.groovy new file mode 100644 index 00000000000..e82c0a34c61 --- /dev/null +++ b/data-tck/src/main/groovy/io/micronaut/data/tck/tests/AbstractCursoredPageSpec.groovy @@ -0,0 +1,256 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.data.tck.tests + +import io.micronaut.data.model.CursoredPageable +import io.micronaut.data.model.Page +import io.micronaut.data.model.Sort +import io.micronaut.data.tck.entities.Book +import io.micronaut.data.tck.entities.Person +import io.micronaut.data.tck.repositories.BookRepository +import io.micronaut.data.tck.repositories.PersonRepository +import spock.lang.Specification + +abstract class AbstractCursoredPageSpec extends Specification { + + abstract PersonRepository getPersonRepository() + + abstract BookRepository getBookRepository() + + abstract void init() + + def setup() { + init() + + // Create a repository that will look something like this: + // id | name | age + // 1 | AAAAA00 | 1 + // 2 | AAAAA01 | 2 + // ... + // 10 | AAAAA09 | 10 + // 11 | BBBBB00 | 1 + // .. + // 260 | ZZZZZ09 | 10 + // 261 | AAAAA00 | 11 + // 262 | AAAAA01 | 12 + // ... + List people = [] + 3.times { + ('A'..'Z').each { letter -> + 10.times { num -> + people << new Person(name: letter * 5 + String.format("%02d", num), age: it * 10 + num + 1) + } + } + } + + personRepository.saveAll(people) + } + + def cleanup() { + personRepository.deleteAll() + } + + void "test cursored pageable list for sorting #sorting"() { + when: "10 people are paged" + def pageable = CursoredPageable.from(10, sorting) + Page page = personRepository.findAll(pageable) + + then: "The data is correct" + page.content.size() == 10 + page.content.every() { it instanceof Person } + page.content[0].name == name1 + page.content[1].name == name2 + page.totalSize == 780 + page.totalPages == 78 + page.nextPageable().startCursor != null + page.previousPageable().endCursor != null + + when: "The next page is selected" + page = personRepository.findAll(page.nextPageable()) + + then: "it is correct" + page.offset == 10 + page.pageNumber == 1 + page.content[0].name == name10 + page.content[9].name == name19 + page.content.size() == 10 + + when: "The previous page is selected" + pageable = page.previousPageable() + page = personRepository.findAll(pageable) + + then: "it is correct" + page.offset == 0 + page.pageNumber == 0 + page.content[0].name == name1 + page.content.size() == 10 + + where: + sorting | name1 | name2 | name10 | name19 + null | "AAAAA00" | "AAAAA01" | "BBBBB00" | "BBBBB09" + Sort.of(Sort.Order.desc("id")) | "ZZZZZ09" | "ZZZZZ08" | "YYYYY09" | "YYYYY00" + Sort.of(Sort.Order.asc("name")) | "AAAAA00" | "AAAAA00" | "AAAAA03" | "AAAAA06" + Sort.of(Sort.Order.desc("name")) | "ZZZZZ09" | "ZZZZZ09" | "ZZZZZ06" | "ZZZZZ03" + Sort.of(Sort.Order.asc("age"), Sort.Order.asc("name")) | "AAAAA00" | "BBBBB00" | "KKKKK00" | "TTTTT00" + Sort.of(Sort.Order.desc("age"), Sort.Order.asc("name")) | "AAAAA09" | "BBBBB09" | "KKKKK09" | "TTTTT09" + } + + void "test pageable list with row removal"() { + when: "10 people are paged" + CursoredPageable.from(10) + def pageable = CursoredPageable.from(10, sorting) + Page page = personRepository.findAll(pageable) + + then: "The data is correct" + page.content.size() == 10 + page.content[0].name == elem1 + page.content[1].name == elem2 + + when: "The next page is selected after deletion" + personRepository.delete(page.content[1]) + personRepository.delete(page.content[9]) + page = personRepository.findAll(page.nextPageable()) + + then: "it is correct" + page.offset == 10 + page.pageNumber == 1 + page.content[0].name == elem10 + page.content[9].name == elem19 + page.content.size() == 10 + + when: "The previous page is selected" + pageable = page.previousPageable() + page = personRepository.findAll(pageable) + + then: "it is correct" + page.offset == 0 + page.pageNumber == 0 + page.content[0].name == elem1 + page.content.size() == 8 + + where: + sorting | elem1 | elem2 | elem10 | elem19 + null | "AAAAA00" | "AAAAA01" | "BBBBB00" | "BBBBB09" + Sort.of(Sort.Order.desc("id")) | "ZZZZZ09" | "ZZZZZ08" | "YYYYY09" | "YYYYY00" + Sort.of(Sort.Order.asc("name")) | "AAAAA00" | "AAAAA00" | "AAAAA03" | "AAAAA06" + Sort.of(Sort.Order.desc("name")) | "ZZZZZ09" | "ZZZZZ09" | "ZZZZZ06" | "ZZZZZ03" + } + + void "test pageable list with row addition"() { + when: "10 people are paged" + def pageable = CursoredPageable.from(10, sorting) + Page page = personRepository.findAll(pageable) + + then: "The data is correct" + page.content.size() == 10 + page.content[0].name == elem1 + page.content[1].name == elem2 + + when: "The next page is selected after deletion" + personRepository.saveAll([ + new Person(name: "AAAAA00"), new Person(name: "AAAAA01"), + new Person(name: "ZZZZZ08"), new Person(name: "ZZZZZ07") + ]) + page = personRepository.findAll(page.nextPageable()) + + then: "it is correct" + page.offset == 10 + page.pageNumber == 1 + page.content[0].name == elem10 + page.content[9].name == elem19 + page.content.size() == 10 + + when: "The previous page is selected" + pageable = page.previousPageable() + page = personRepository.findAll(pageable) + + then: "it is correct" + page.offset == 0 + page.pageNumber == 0 + page.content[0].name == elem3 + page.content.size() == 10 + + when: "The second previous page is selected" + page = personRepository.findAll(page.previousPageable()) + + then: + page.offset == 0 + page.pageNumber == 0 + page.content[0].name == elem1 + page.content[1].name == elem2 + page.content.size() == 2 + + where: + sorting | elem1 | elem2 | elem3 | elem10 | elem19 + Sort.of(Sort.Order.asc("name")) | "AAAAA00" | "AAAAA00" | "AAAAA00" | "AAAAA03" | "AAAAA06" + Sort.of(Sort.Order.desc("name")) | "ZZZZZ09" | "ZZZZZ09" | "ZZZZZ09" | "ZZZZZ06" | "ZZZZZ03" + } + + void "test pageable findBy"() { + when: "People are searched for" + def pageable = CursoredPageable.from(10, null) + Page page = personRepository.findByNameLike("A%", pageable) + Page page2 = personRepository.findPeople("A%", pageable) + + then: "The page is correct" + page.offset == 0 + page.pageNumber == 0 + page.totalSize == 30 + page2.totalSize == page.totalSize + var firstContent = page.content + page.content.name.every{ it.startsWith("A") } + + when: "The next page is retrieved" + page = personRepository.findByNameLike("A%", page.nextPageable()) + + then: "it is correct" + page.offset == 10 + page.pageNumber == 1 + page.content.id != firstContent.id + page.content.name.every{ it.startsWith("A") } + + when: "The previous page is selected" + pageable = page.previousPageable() + page = personRepository.findByNameLike("A%", pageable) + + then: "it is correct" + page.offset == 0 + page.pageNumber == 0 + page.content.size() == 10 + page.content.id == firstContent.id + page.content.name.every{ it.startsWith("A") } + } + + void "test find with left join"() { + given: + def books = bookRepository.saveAll([ + new Book(title: "Book 1", totalPages: 100), + new Book(title: "Book 2", totalPages: 100) + ]) + + when: + def page = bookRepository.findByTotalPagesGreaterThan( + 50, CursoredPageable.from(books.size(), null) + ) + + then: + page.getContent().size() == books.size() + page.getTotalSize() == books.size() + + cleanup: + bookRepository.deleteAll() + } +} From 2a88b3efa9dc8d6fc023fe1641ec34b943c65b98 Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Mon, 15 Apr 2024 11:11:16 -0400 Subject: [PATCH 02/20] Checkstyle --- .../data/model/query/builder/AbstractSqlLikeQueryBuilder.java | 1 + .../io/micronaut/data/tck/tests/AbstractCursoredPageSpec.groovy | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/data-model/src/main/java/io/micronaut/data/model/query/builder/AbstractSqlLikeQueryBuilder.java b/data-model/src/main/java/io/micronaut/data/model/query/builder/AbstractSqlLikeQueryBuilder.java index 74eeffe340f..672d01e567b 100644 --- a/data-model/src/main/java/io/micronaut/data/model/query/builder/AbstractSqlLikeQueryBuilder.java +++ b/data-model/src/main/java/io/micronaut/data/model/query/builder/AbstractSqlLikeQueryBuilder.java @@ -1999,6 +1999,7 @@ public QueryResult buildOrderBy(String query, @NonNull PersistentEntity entity, * For example, property name might be encoded as {@code `person_.name`} using * its path and table's alias. * + * @param propertyName The name of the property * @param query The query * @param entity The root entity * @param annotationMetadata The annotation metadata diff --git a/data-tck/src/main/groovy/io/micronaut/data/tck/tests/AbstractCursoredPageSpec.groovy b/data-tck/src/main/groovy/io/micronaut/data/tck/tests/AbstractCursoredPageSpec.groovy index e82c0a34c61..c3597545d7c 100644 --- a/data-tck/src/main/groovy/io/micronaut/data/tck/tests/AbstractCursoredPageSpec.groovy +++ b/data-tck/src/main/groovy/io/micronaut/data/tck/tests/AbstractCursoredPageSpec.groovy @@ -110,7 +110,6 @@ abstract class AbstractCursoredPageSpec extends Specification { void "test pageable list with row removal"() { when: "10 people are paged" - CursoredPageable.from(10) def pageable = CursoredPageable.from(10, sorting) Page page = personRepository.findAll(pageable) From 34f73e7456a728b38083519f35362902d76092e1 Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Mon, 15 Apr 2024 12:34:38 -0400 Subject: [PATCH 03/20] Change base to 4.8.x --- .../java/io/micronaut/data/model/CursoredPageable.java | 2 +- .../operations/internal/sql/DefaultSqlPreparedQuery.java | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/data-model/src/main/java/io/micronaut/data/model/CursoredPageable.java b/data-model/src/main/java/io/micronaut/data/model/CursoredPageable.java index e89e3948b63..e3fd02c131d 100644 --- a/data-model/src/main/java/io/micronaut/data/model/CursoredPageable.java +++ b/data-model/src/main/java/io/micronaut/data/model/CursoredPageable.java @@ -29,7 +29,7 @@ * Models pageable data that uses a cursor. * * @author Andriy Dmytruk - * @since 4.6.1 + * @since 4.8.0 */ @Serdeable @Introspected diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/DefaultSqlPreparedQuery.java b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/DefaultSqlPreparedQuery.java index fe1333df7b0..2868d05d4a2 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/DefaultSqlPreparedQuery.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/DefaultSqlPreparedQuery.java @@ -41,6 +41,7 @@ import java.lang.reflect.Array; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; @@ -172,7 +173,13 @@ public void attachPageable(Pageable pageable, boolean isSingleResult) { // Create a sort for the cursored pagination. The sort must produce a unique // sorting on the rows. Therefore, we make sure id is present in it. List orders = new ArrayList<>(sort.getOrderBy()); - for (PersistentProperty idProperty: persistentEntity.getIdentityProperties()) { + List> idProperties; + if (persistentEntity.getIdentity() != null) { + idProperties = List.of(persistentEntity.getIdentity()); + } else { + idProperties = Arrays.stream(persistentEntity.getCompositeIdentity()).toList(); + } + for (PersistentProperty idProperty: idProperties) { String name = idProperty.getName(); if (orders.stream().noneMatch(o -> o.getProperty().equals(name))) { orders.add(Order.asc(name)); From b5294fc92d7a41ae9eb4d04ffc018ba6401b8f85 Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Mon, 15 Apr 2024 14:03:43 -0400 Subject: [PATCH 04/20] Add hasNext and hasPrevious methods to Page --- .../data/model/CursoredPageable.java | 14 ++++++++++ .../data/model/DefaultCursoredPageable.java | 10 +++++++ .../java/io/micronaut/data/model/Page.java | 26 +++++++++++++++++++ .../tck/tests/AbstractCursoredPageSpec.groovy | 17 ++++++++++-- .../data/tck/tests/AbstractPageSpec.groovy | 6 +++++ 5 files changed, 71 insertions(+), 2 deletions(-) diff --git a/data-model/src/main/java/io/micronaut/data/model/CursoredPageable.java b/data-model/src/main/java/io/micronaut/data/model/CursoredPageable.java index e3fd02c131d..40cc432ada2 100644 --- a/data-model/src/main/java/io/micronaut/data/model/CursoredPageable.java +++ b/data-model/src/main/java/io/micronaut/data/model/CursoredPageable.java @@ -74,6 +74,20 @@ public interface CursoredPageable extends Pageable { throw new IllegalStateException("To get the next CursoredPageable, you must retrieve this one from a page"); } + /** + * @return Whether there is a next page + */ + default boolean hasNext() { + return false; + } + + /** + * @return Whether there is a previous page. + */ + default boolean hasPrevious() { + return false; + } + /** * Creates a new {@link CursoredPageable} with the given sort. * diff --git a/data-model/src/main/java/io/micronaut/data/model/DefaultCursoredPageable.java b/data-model/src/main/java/io/micronaut/data/model/DefaultCursoredPageable.java index f05dcbff6f5..74a958a16ab 100644 --- a/data-model/src/main/java/io/micronaut/data/model/DefaultCursoredPageable.java +++ b/data-model/src/main/java/io/micronaut/data/model/DefaultCursoredPageable.java @@ -128,6 +128,16 @@ public CursoredPageable previous() { return CursoredPageable.super.previous(); } + @Override + public boolean hasNext() { + return endCursor != null; + } + + @Override + public boolean hasPrevious() { + return startCursor != null; + } + @Override public boolean equals(Object o) { if (this == o) { diff --git a/data-model/src/main/java/io/micronaut/data/model/Page.java b/data-model/src/main/java/io/micronaut/data/model/Page.java index 9dd670252db..467cc680c1c 100644 --- a/data-model/src/main/java/io/micronaut/data/model/Page.java +++ b/data-model/src/main/java/io/micronaut/data/model/Page.java @@ -63,6 +63,32 @@ default int getTotalPages() { return size == 0 ? 1 : (int) Math.ceil((double) getTotalSize() / (double) size); } + /** + * Determines whether there is a next page. + * + * @since 4.8.0 + * @return Whether there exist a next page. + */ + default boolean hasNext() { + if (getPageable() instanceof CursoredPageable cursoredPageable) { + return cursoredPageable.hasNext(); + } + return getOffset() + getSize() < getTotalSize(); + } + + /** + * Determines whether there is a previous page. + * + * @since 4.8.0 + * @return Whether there exist a previous page. + */ + default boolean hasPrevious() { + if (getPageable() instanceof CursoredPageable cursoredPageable) { + return cursoredPageable.hasPrevious(); + } + return getOffset() > 0; + } + /** * Maps the content with the given function. * diff --git a/data-tck/src/main/groovy/io/micronaut/data/tck/tests/AbstractCursoredPageSpec.groovy b/data-tck/src/main/groovy/io/micronaut/data/tck/tests/AbstractCursoredPageSpec.groovy index c3597545d7c..7131e039d5e 100644 --- a/data-tck/src/main/groovy/io/micronaut/data/tck/tests/AbstractCursoredPageSpec.groovy +++ b/data-tck/src/main/groovy/io/micronaut/data/tck/tests/AbstractCursoredPageSpec.groovy @@ -75,8 +75,7 @@ abstract class AbstractCursoredPageSpec extends Specification { page.content[1].name == name2 page.totalSize == 780 page.totalPages == 78 - page.nextPageable().startCursor != null - page.previousPageable().endCursor != null + page.hasNext() when: "The next page is selected" page = personRepository.findAll(page.nextPageable()) @@ -87,6 +86,8 @@ abstract class AbstractCursoredPageSpec extends Specification { page.content[0].name == name10 page.content[9].name == name19 page.content.size() == 10 + page.hasNext() + page.hasPrevious() when: "The previous page is selected" pageable = page.previousPageable() @@ -97,6 +98,8 @@ abstract class AbstractCursoredPageSpec extends Specification { page.pageNumber == 0 page.content[0].name == name1 page.content.size() == 10 + page.hasNext() + page.hasPrevious() where: sorting | name1 | name2 | name10 | name19 @@ -117,6 +120,7 @@ abstract class AbstractCursoredPageSpec extends Specification { page.content.size() == 10 page.content[0].name == elem1 page.content[1].name == elem2 + page.hasNext() when: "The next page is selected after deletion" personRepository.delete(page.content[1]) @@ -129,6 +133,8 @@ abstract class AbstractCursoredPageSpec extends Specification { page.content[0].name == elem10 page.content[9].name == elem19 page.content.size() == 10 + page.hasNext() + page.hasPrevious() when: "The previous page is selected" pageable = page.previousPageable() @@ -139,6 +145,8 @@ abstract class AbstractCursoredPageSpec extends Specification { page.pageNumber == 0 page.content[0].name == elem1 page.content.size() == 8 + !page.hasPrevious() + page.hasNext() where: sorting | elem1 | elem2 | elem10 | elem19 @@ -157,6 +165,7 @@ abstract class AbstractCursoredPageSpec extends Specification { page.content.size() == 10 page.content[0].name == elem1 page.content[1].name == elem2 + page.hasNext() when: "The next page is selected after deletion" personRepository.saveAll([ @@ -171,6 +180,8 @@ abstract class AbstractCursoredPageSpec extends Specification { page.content[0].name == elem10 page.content[9].name == elem19 page.content.size() == 10 + page.hasNext() + page.hasPrevious() when: "The previous page is selected" pageable = page.previousPageable() @@ -181,6 +192,7 @@ abstract class AbstractCursoredPageSpec extends Specification { page.pageNumber == 0 page.content[0].name == elem3 page.content.size() == 10 + page.hasPrevious() when: "The second previous page is selected" page = personRepository.findAll(page.previousPageable()) @@ -191,6 +203,7 @@ abstract class AbstractCursoredPageSpec extends Specification { page.content[0].name == elem1 page.content[1].name == elem2 page.content.size() == 2 + !page.hasPrevious() where: sorting | elem1 | elem2 | elem3 | elem10 | elem19 diff --git a/data-tck/src/main/groovy/io/micronaut/data/tck/tests/AbstractPageSpec.groovy b/data-tck/src/main/groovy/io/micronaut/data/tck/tests/AbstractPageSpec.groovy index c1762444bc4..282400f0ceb 100644 --- a/data-tck/src/main/groovy/io/micronaut/data/tck/tests/AbstractPageSpec.groovy +++ b/data-tck/src/main/groovy/io/micronaut/data/tck/tests/AbstractPageSpec.groovy @@ -81,6 +81,8 @@ abstract class AbstractPageSpec extends Specification { page.totalPages == 130 page.nextPageable().offset == 10 page.nextPageable().size == 10 + page.hasNext() + !page.hasPrevious() when: "The next page is selected" pageable = page.nextPageable() @@ -91,6 +93,8 @@ abstract class AbstractPageSpec extends Specification { page.pageNumber == 1 page.content[0].name.startsWith("K") page.content.size() == 10 + page.hasNext() + page.hasPrevious() when: "The previous page is selected" pageable = page.previousPageable() @@ -101,6 +105,8 @@ abstract class AbstractPageSpec extends Specification { page.pageNumber == 0 page.content[0].name.startsWith("A") page.content.size() == 10 + page.hasNext() + !page.hasPrevious() } void "test pageable sort"() { From f5d2c5a792e30a6c11329e7b3d810afbc477e282 Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Mon, 15 Apr 2024 14:13:25 -0400 Subject: [PATCH 05/20] Checkstyle fixes and disable PostgreSQL test --- .../data/jdbc/postgres/PostgresCursoredPaginationSpec.groovy | 2 ++ .../data/runtime/intercept/DefaultFindPageInterceptor.java | 2 -- .../intercept/criteria/AbstractSpecificationInterceptor.java | 1 - .../data/runtime/mapper/sql/SqlResultEntityTypeMapper.java | 1 - .../operations/internal/sql/DefaultSqlPreparedQuery.java | 4 +++- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/postgres/PostgresCursoredPaginationSpec.groovy b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/postgres/PostgresCursoredPaginationSpec.groovy index 9020aeae5bc..1830bec63f7 100644 --- a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/postgres/PostgresCursoredPaginationSpec.groovy +++ b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/postgres/PostgresCursoredPaginationSpec.groovy @@ -20,8 +20,10 @@ import io.micronaut.data.tck.repositories.BookRepository import io.micronaut.data.tck.repositories.PersonRepository import io.micronaut.data.tck.tests.AbstractCursoredPageSpec import spock.lang.AutoCleanup +import spock.lang.Ignore import spock.lang.Shared +@Ignore("Causes error: 'FATAL: sorry, too many clients already'") class PostgresCursoredPaginationSpec extends AbstractCursoredPageSpec implements PostgresTestPropertyProvider { @Shared @AutoCleanup ApplicationContext context diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/DefaultFindPageInterceptor.java b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/DefaultFindPageInterceptor.java index 367b1103e63..fc0bd23a8ce 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/DefaultFindPageInterceptor.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/DefaultFindPageInterceptor.java @@ -21,14 +21,12 @@ import io.micronaut.data.annotation.Query; import io.micronaut.data.intercept.FindPageInterceptor; import io.micronaut.data.intercept.RepositoryMethodKey; -import io.micronaut.data.model.CursoredPageable; import io.micronaut.data.model.Page; import io.micronaut.data.model.Pageable; import io.micronaut.data.model.runtime.PreparedQuery; import io.micronaut.data.operations.RepositoryOperations; import io.micronaut.data.runtime.operations.internal.sql.DefaultSqlPreparedQuery; -import java.util.Collections; import java.util.List; /** diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/criteria/AbstractSpecificationInterceptor.java b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/criteria/AbstractSpecificationInterceptor.java index 4cca0c74517..e6ae8060965 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/criteria/AbstractSpecificationInterceptor.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/criteria/AbstractSpecificationInterceptor.java @@ -64,7 +64,6 @@ import java.util.ArrayList; import java.util.Collection; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/mapper/sql/SqlResultEntityTypeMapper.java b/data-runtime/src/main/java/io/micronaut/data/runtime/mapper/sql/SqlResultEntityTypeMapper.java index 2d0f51fe3e0..19625ef9601 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/mapper/sql/SqlResultEntityTypeMapper.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/mapper/sql/SqlResultEntityTypeMapper.java @@ -46,7 +46,6 @@ import java.sql.SQLException; import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.ListIterator; diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/DefaultSqlPreparedQuery.java b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/DefaultSqlPreparedQuery.java index 2868d05d4a2..ba6374c2193 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/DefaultSqlPreparedQuery.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/DefaultSqlPreparedQuery.java @@ -251,8 +251,9 @@ private String buildCursorPagination(@Nullable List cursor, @NonNull Sor for (Order order: orders) { cursorProperties.add(getPersistentEntity().getPropertyByName(order.getProperty())); } - if (cursor == null) + if (cursor == null) { return ""; + } if (orders.size() != cursor.size()) { throw new IllegalArgumentException("The cursor must match the sorting size"); } @@ -310,6 +311,7 @@ private String buildCursorPagination(@Nullable List cursor, @NonNull Sor * * @param results The scanning results * @param pageable The pageable sent by user + * @param totalSize The total count * @return The updated pageable */ public Pageable updatePageable(List results, Pageable pageable, long totalSize) { From 2e8e81375a66962d4b807e7113dc8621335306ad Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Tue, 16 Apr 2024 14:38:11 -0400 Subject: [PATCH 06/20] Implement some review comments --- .../mysql/MysqlCursoredPaginationSpec.groovy | 47 ++++++++++++++++++ .../oraclexe/OracleXEPaginationSpec.groovy | 47 ++++++++++++++++++ .../PostgresCursoredPaginationSpec.groovy | 3 ++ .../sqlserver/SqlServerPaginationSpec.groovy | 3 ++ .../data/model/CursoredPageable.java | 8 ++- .../data/model/DefaultCursoredPageable.java | 49 +++++++++---------- .../internal/sql/DefaultSqlPreparedQuery.java | 34 +++++-------- 7 files changed, 142 insertions(+), 49 deletions(-) create mode 100644 data-jdbc/src/test/groovy/io/micronaut/data/jdbc/mysql/MysqlCursoredPaginationSpec.groovy create mode 100644 data-jdbc/src/test/groovy/io/micronaut/data/jdbc/oraclexe/OracleXEPaginationSpec.groovy diff --git a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/mysql/MysqlCursoredPaginationSpec.groovy b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/mysql/MysqlCursoredPaginationSpec.groovy new file mode 100644 index 00000000000..e65c34e7d1b --- /dev/null +++ b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/mysql/MysqlCursoredPaginationSpec.groovy @@ -0,0 +1,47 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.data.jdbc.mysql + +import groovy.transform.Memoized +import io.micronaut.context.ApplicationContext +import io.micronaut.data.tck.repositories.BookRepository +import io.micronaut.data.tck.repositories.PersonRepository +import io.micronaut.data.tck.tests.AbstractCursoredPageSpec +import spock.lang.AutoCleanup +import spock.lang.Shared + +class MysqlCursoredPaginationSpec extends AbstractCursoredPageSpec implements MySQLTestPropertyProvider { + + @Shared @AutoCleanup ApplicationContext context + + @Memoized + @Override + PersonRepository getPersonRepository() { + return context.getBean(MySqlPersonRepository) + } + + @Memoized + @Override + BookRepository getBookRepository() { + return context.getBean(MySqlBookRepository) + } + + @Override + void init() { + context = ApplicationContext.run(properties) + } + +} diff --git a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/oraclexe/OracleXEPaginationSpec.groovy b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/oraclexe/OracleXEPaginationSpec.groovy new file mode 100644 index 00000000000..acf52db8bba --- /dev/null +++ b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/oraclexe/OracleXEPaginationSpec.groovy @@ -0,0 +1,47 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.data.jdbc.oraclexe + +import groovy.transform.Memoized +import io.micronaut.context.ApplicationContext +import io.micronaut.data.tck.repositories.BookRepository +import io.micronaut.data.tck.repositories.PersonRepository +import io.micronaut.data.tck.tests.AbstractCursoredPageSpec +import spock.lang.AutoCleanup +import spock.lang.Shared + +class OracleXEPaginationSpec extends AbstractCursoredPageSpec implements OracleTestPropertyProvider { + + @Shared @AutoCleanup ApplicationContext context + + @Override + @Memoized + PersonRepository getPersonRepository() { + return context.getBean(OracleXEPersonRepository) + } + + @Override + @Memoized + BookRepository getBookRepository() { + return context.getBean(OracleXEBookRepository) + } + + @Override + void init() { + context = ApplicationContext.run(properties) + } + +} diff --git a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/postgres/PostgresCursoredPaginationSpec.groovy b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/postgres/PostgresCursoredPaginationSpec.groovy index 1830bec63f7..214b741c505 100644 --- a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/postgres/PostgresCursoredPaginationSpec.groovy +++ b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/postgres/PostgresCursoredPaginationSpec.groovy @@ -15,6 +15,7 @@ */ package io.micronaut.data.jdbc.postgres +import groovy.transform.Memoized import io.micronaut.context.ApplicationContext import io.micronaut.data.tck.repositories.BookRepository import io.micronaut.data.tck.repositories.PersonRepository @@ -27,11 +28,13 @@ import spock.lang.Shared class PostgresCursoredPaginationSpec extends AbstractCursoredPageSpec implements PostgresTestPropertyProvider { @Shared @AutoCleanup ApplicationContext context + @Memoized @Override PersonRepository getPersonRepository() { return context.getBean(PostgresPersonRepository) } + @Memoized @Override BookRepository getBookRepository() { return context.getBean(PostgresBookRepository) diff --git a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/sqlserver/SqlServerPaginationSpec.groovy b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/sqlserver/SqlServerPaginationSpec.groovy index 30bc7f46789..faf4bb6f0f8 100644 --- a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/sqlserver/SqlServerPaginationSpec.groovy +++ b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/sqlserver/SqlServerPaginationSpec.groovy @@ -15,6 +15,7 @@ */ package io.micronaut.data.jdbc.sqlserver +import groovy.transform.Memoized import io.micronaut.context.ApplicationContext import io.micronaut.data.tck.repositories.BookRepository import io.micronaut.data.tck.repositories.PersonRepository @@ -26,11 +27,13 @@ class SqlServerPaginationSpec extends AbstractPageSpec implements MSSQLTestPrope @Shared @AutoCleanup ApplicationContext context + @Memoized @Override PersonRepository getPersonRepository() { return context.getBean(MSSQLPersonRepository) } + @Memoized @Override BookRepository getBookRepository() { return context.getBean(MSBookRepository) diff --git a/data-model/src/main/java/io/micronaut/data/model/CursoredPageable.java b/data-model/src/main/java/io/micronaut/data/model/CursoredPageable.java index 40cc432ada2..5b3ed0bd9a9 100644 --- a/data-model/src/main/java/io/micronaut/data/model/CursoredPageable.java +++ b/data-model/src/main/java/io/micronaut/data/model/CursoredPageable.java @@ -40,7 +40,7 @@ public interface CursoredPageable extends Pageable { * Constant for no pagination. */ CursoredPageable UNPAGED = new DefaultCursoredPageable( - 0, null, null, false, -1, null + 0, null, null, false, -1, Sort.UNSORTED ); /** @@ -114,6 +114,9 @@ default boolean hasPrevious() { @JsonProperty("size") int size, @JsonProperty("sort") @Nullable Sort sort ) { + if (sort == null) { + sort = UNSORTED; + } return new DefaultCursoredPageable(0, null, null, false, size, sort); } @@ -137,6 +140,9 @@ default boolean hasPrevious() { @JsonProperty("size") int size, @JsonProperty("sort") @Nullable Sort sort ) { + if (sort == null) { + sort = UNSORTED; + } return new DefaultCursoredPageable(page, startCursor, endCursor, isBackward, size, sort); } diff --git a/data-model/src/main/java/io/micronaut/data/model/DefaultCursoredPageable.java b/data-model/src/main/java/io/micronaut/data/model/DefaultCursoredPageable.java index 74a958a16ab..c016300beb8 100644 --- a/data-model/src/main/java/io/micronaut/data/model/DefaultCursoredPageable.java +++ b/data-model/src/main/java/io/micronaut/data/model/DefaultCursoredPageable.java @@ -27,17 +27,19 @@ * The default cursored pageable implementation. * * @author Andriy Dmytruk - * @since 4.6.1 + * @since 4.8.0 */ @Introspected -final class DefaultCursoredPageable implements CursoredPageable { - - private final int max; - private final int number; - private final List startCursor; - private final List endCursor; - private final boolean isBackward; - private final Sort sort; +record DefaultCursoredPageable( + int size, + @Nullable + List startCursor, + @Nullable + List endCursor, + boolean isBackward, + int page, + Sort sort +) implements CursoredPageable { /** * Default constructor. @@ -52,29 +54,23 @@ final class DefaultCursoredPageable implements CursoredPageable { * @param sort The sorting */ @Creator - DefaultCursoredPageable(int page, @Nullable List startCursor, @Nullable List endCursor, boolean isBackward, int size, @Nullable Sort sort) { + DefaultCursoredPageable { if (page < 0) { throw new IllegalArgumentException("Page index cannot be negative"); } if (size == 0) { throw new IllegalArgumentException("Size cannot be 0"); } - this.max = size; - this.number = page; - this.startCursor = startCursor; - this.endCursor = endCursor; - this.isBackward = isBackward; - this.sort = sort == null ? Sort.unsorted() : sort; } @Override public int getSize() { - return max; + return size; } @Override public int getNumber() { - return number; + return page; } @NonNull @@ -102,7 +98,7 @@ public boolean isBackward() { public CursoredPageable next() { if (endCursor != null) { return new DefaultCursoredPageable( - number + 1, + page + 1, endCursor, null, false, @@ -117,7 +113,7 @@ public CursoredPageable next() { public CursoredPageable previous() { if (startCursor != null) { return new DefaultCursoredPageable( - Math.max(number - 1, 0), + Math.max(page - 1, 0), null, startCursor, true, @@ -146,21 +142,22 @@ public boolean equals(Object o) { if (!(o instanceof DefaultCursoredPageable that)) { return false; } - return max == that.max && - Objects.equals(startCursor, that.startCursor) && - Objects.equals(sort, that.sort); + return size == that.size + && Objects.equals(startCursor, that.startCursor) + && Objects.equals(endCursor, that.endCursor) + && Objects.equals(sort, that.sort); } @Override public int hashCode() { - return Objects.hash(max, startCursor, sort); + return Objects.hash(size, startCursor, endCursor, sort); } @Override public String toString() { return "DefaultCursoredPageable{" + - "max=" + max + - ", number=" + number + + "size=" + size + + ", number=" + page + ", startCursor=" + startCursor + ", endCursor=" + endCursor + ", sort=" + sort + diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/DefaultSqlPreparedQuery.java b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/DefaultSqlPreparedQuery.java index ba6374c2193..698c5de96b0 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/DefaultSqlPreparedQuery.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/DefaultSqlPreparedQuery.java @@ -226,7 +226,7 @@ private Sort reverseSort(Sort sort) { if (!sort.isSorted()) { return sort; } - List orders = new ArrayList<>(); + List orders = new ArrayList<>(sort.getOrderBy().size()); for (Order order : sort.getOrderBy()) { orders.add(new Order( order.getProperty(), @@ -247,7 +247,7 @@ private Sort reverseSort(Sort sort) { @NonNull private String buildCursorPagination(@Nullable List cursor, @NonNull Sort sort) { List orders = sort.getOrderBy(); - cursorProperties = new ArrayList<>(); + cursorProperties = new ArrayList<>(orders.size()); for (Order order: orders) { cursorProperties.add(getPersistentEntity().getPropertyByName(order.getProperty())); } @@ -261,9 +261,9 @@ private String buildCursorPagination(@Nullable List cursor, @NonNull Sor throw new IllegalArgumentException("At least one sorting property must be supplied"); } - List cursorBindings = new ArrayList<>(); - cursorQueryBindings = new ArrayList<>(); - for (int i = 0; i < cursor.size(); ++i) { + List cursorBindings = new ArrayList<>(orders.size()); + cursorQueryBindings = new ArrayList<>(orders.size() * (orders.size() + 1) / 2); + for (int i = 0; i < orders.size(); ++i) { cursorBindings.add(new CursoredQueryParameterBinder( "cursor_" + i, cursorProperties.get(i).getDataType(), cursor.get(i) )); @@ -275,7 +275,7 @@ private String buildCursorPagination(@Nullable List cursor, @NonNull Sor query = query.substring(0, i) + "(" + query.substring(i) + ")"; builder.append(" AND ("); } else { - builder.append("WHERE "); + builder.append("WHERE ("); } for (int i = 0; i < orders.size(); ++i) { builder.append("("); @@ -298,10 +298,7 @@ private String buildCursorPagination(@Nullable List cursor, @NonNull Sor builder.append(" OR "); } } - - if (query.contains("WHERE")) { - builder.append(")"); - } + builder.append(")"); return builder.toString(); } @@ -420,18 +417,11 @@ protected int sizeOf(Object value) { return 1; } - protected static class CursoredQueryParameterBinder implements QueryParameterBinding { - - private final String name; - private DataType dataType; - private final Object value; - - public CursoredQueryParameterBinder(String name, DataType dataType, Object value) { - this.name = name; - this.dataType = dataType; - this.value = value; - } - + private record CursoredQueryParameterBinder( + String name, + DataType dataType, + Object value + ) implements QueryParameterBinding { @Override public String getName() { return name; From 9964e4c0cb14d80eb4d45bcbe858c4027acd53d7 Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Tue, 16 Apr 2024 15:58:57 -0400 Subject: [PATCH 07/20] Add methods corresponding to the jakarta PageRequest API --- .../data/model/CursoredPageable.java | 46 +++++- .../data/model/DefaultCursoredPageable.java | 17 +- .../micronaut/data/model/DefaultPageable.java | 11 ++ .../io/micronaut/data/model/Pageable.java | 148 ++++++++++++++++-- .../internal/sql/DefaultSqlPreparedQuery.java | 15 +- 5 files changed, 196 insertions(+), 41 deletions(-) diff --git a/data-model/src/main/java/io/micronaut/data/model/CursoredPageable.java b/data-model/src/main/java/io/micronaut/data/model/CursoredPageable.java index 5b3ed0bd9a9..a1e4f5a187e 100644 --- a/data-model/src/main/java/io/micronaut/data/model/CursoredPageable.java +++ b/data-model/src/main/java/io/micronaut/data/model/CursoredPageable.java @@ -24,6 +24,7 @@ import io.micronaut.serde.annotation.Serdeable; import java.util.List; +import java.util.Optional; /** * Models pageable data that uses a cursor. @@ -40,7 +41,7 @@ public interface CursoredPageable extends Pageable { * Constant for no pagination. */ CursoredPageable UNPAGED = new DefaultCursoredPageable( - 0, null, null, false, -1, Sort.UNSORTED + -1, null, null, false, 0, Sort.UNSORTED ); /** @@ -48,14 +49,14 @@ public interface CursoredPageable extends Pageable { * This cursor is used for forward pagination. */ @Nullable - List getStartCursor(); + Cursor getStartCursor(); /** * @return The cursor values corresponding to the end of queried data. * This cursor is used for backward pagination. */ @Nullable - List getEndCursor(); + Cursor getEndCursor(); /** * Whether the pageable is traversing backwards. @@ -64,6 +65,16 @@ public interface CursoredPageable extends Pageable { */ boolean isBackward(); + @Override + default Mode getMode() { + return isBackward() ? Mode.CURSOR_PREVIOUS : Mode.CURSOR_NEXT; + } + + @Override + default Optional cursor() { + return isBackward() ? Optional.ofNullable(getEndCursor()) : Optional.ofNullable(getStartCursor()); + } + @Override default @NonNull CursoredPageable next() { throw new IllegalStateException("To get the next CursoredPageable, you must retrieve this one from a page"); @@ -99,7 +110,7 @@ default boolean hasPrevious() { return UNPAGED; } return new DefaultCursoredPageable( - 0, null, null, false, -1, sort + -1, null, null, false, 0, sort ); } @@ -117,7 +128,7 @@ default boolean hasPrevious() { if (sort == null) { sort = UNSORTED; } - return new DefaultCursoredPageable(0, null, null, false, size, sort); + return new DefaultCursoredPageable(size, null, null, false, 0, sort); } /** @@ -134,8 +145,8 @@ default boolean hasPrevious() { @JsonCreator static @NonNull CursoredPageable from( @JsonProperty("number") int page, - @JsonProperty("startCursor") @Nullable List startCursor, - @JsonProperty("endCursor") @Nullable List endCursor, + @JsonProperty("startCursor") @Nullable Cursor startCursor, + @JsonProperty("endCursor") @Nullable Cursor endCursor, @JsonProperty(value = "isBackward", defaultValue = "false") boolean isBackward, @JsonProperty("size") int size, @JsonProperty("sort") @Nullable Sort sort @@ -143,7 +154,7 @@ default boolean hasPrevious() { if (sort == null) { sort = UNSORTED; } - return new DefaultCursoredPageable(page, startCursor, endCursor, isBackward, size, sort); + return new DefaultCursoredPageable(size, startCursor, endCursor, isBackward, page, sort); } /** @@ -152,4 +163,23 @@ default boolean hasPrevious() { static @NonNull CursoredPageable unpaged() { return UNPAGED; } + + /** + * Default implementation of the {@link Cursor}. + * + * @param elements The cursor elements + */ + record DefaultCursor( + List elements + ) implements Cursor { + @Override + public Object get(int index) { + return elements.get(index); + } + + @Override + public int size() { + return elements.size(); + } + } } diff --git a/data-model/src/main/java/io/micronaut/data/model/DefaultCursoredPageable.java b/data-model/src/main/java/io/micronaut/data/model/DefaultCursoredPageable.java index c016300beb8..fc9b2b7dc7e 100644 --- a/data-model/src/main/java/io/micronaut/data/model/DefaultCursoredPageable.java +++ b/data-model/src/main/java/io/micronaut/data/model/DefaultCursoredPageable.java @@ -20,7 +20,6 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; -import java.util.List; import java.util.Objects; /** @@ -33,9 +32,9 @@ record DefaultCursoredPageable( int size, @Nullable - List startCursor, + Cursor startCursor, @Nullable - List endCursor, + Cursor endCursor, boolean isBackward, int page, Sort sort @@ -80,12 +79,12 @@ public Sort getSort() { } @Override - public List getStartCursor() { + public Cursor getStartCursor() { return startCursor; } @Override - public List getEndCursor() { + public Cursor getEndCursor() { return endCursor; } @@ -98,11 +97,11 @@ public boolean isBackward() { public CursoredPageable next() { if (endCursor != null) { return new DefaultCursoredPageable( - page + 1, + getSize(), endCursor, null, false, - getSize(), + page + 1, getSort() ); } @@ -113,11 +112,11 @@ public CursoredPageable next() { public CursoredPageable previous() { if (startCursor != null) { return new DefaultCursoredPageable( - Math.max(page - 1, 0), + getSize(), null, startCursor, true, - getSize(), + Math.max(page - 1, 0), getSort() ); } diff --git a/data-model/src/main/java/io/micronaut/data/model/DefaultPageable.java b/data-model/src/main/java/io/micronaut/data/model/DefaultPageable.java index b4d8b877e3d..f07d8b52317 100644 --- a/data-model/src/main/java/io/micronaut/data/model/DefaultPageable.java +++ b/data-model/src/main/java/io/micronaut/data/model/DefaultPageable.java @@ -21,6 +21,7 @@ import io.micronaut.core.annotation.NonNull; import java.util.Objects; +import java.util.Optional; /** * The default pageable implementation. @@ -65,6 +66,16 @@ public int getNumber() { return number; } + @Override + public Mode getMode() { + return Mode.OFFSET; + } + + @Override + public Optional cursor() { + return Optional.empty(); + } + @NonNull @Override public Sort getSort() { diff --git a/data-model/src/main/java/io/micronaut/data/model/Pageable.java b/data-model/src/main/java/io/micronaut/data/model/Pageable.java index 185b0657d27..893f4f57383 100644 --- a/data-model/src/main/java/io/micronaut/data/model/Pageable.java +++ b/data-model/src/main/java/io/micronaut/data/model/Pageable.java @@ -24,7 +24,9 @@ import io.micronaut.core.annotation.Nullable; import io.micronaut.serde.annotation.Serdeable; +import java.util.Arrays; import java.util.List; +import java.util.Optional; /** * Models pageable data. The {@link #from(int, int)} method can be used to construct a new instance to pass to Micronaut Data methods. @@ -47,6 +49,16 @@ public int getNumber() { return 0; } + @Override + public Mode getMode() { + return Mode.OFFSET; + } + + @Override + public Optional cursor() { + return Optional.empty(); + } + @Override public int getSize() { return -1; @@ -64,6 +76,19 @@ public int getSize() { */ int getSize(); + /** + * The pagination mode that is either offset pagination, cursor forward or cursor backward + * pagination. + * @return The pagination mode + */ + Mode getMode(); + + /** + * Get the cursor in case cursored pagination is used. + * @return The cursor + */ + Optional cursor(); + /** * Offset in the requested collection. Defaults to zero. * @return offset in the requested collection @@ -206,23 +231,7 @@ default List getOrderBy() { if (sort == null) { return UNPAGED; } else { - return new Pageable() { - @Override - public int getNumber() { - return 0; - } - - @Override - public int getSize() { - return -1; - } - - @NonNull - @Override - public Sort getSort() { - return sort; - } - }; + return new DefaultPageable(0, -1, sort); } } @@ -232,4 +241,109 @@ public Sort getSort() { static @NonNull Pageable unpaged() { return UNPAGED; } + + /** + * Create a new {@link Pageable} for forward pagination given the cursor after which to query. + * + * @param cursor The cursor + * @param page The page number + * @param size The page size + * @param sort The sorting + * @return The pageable + */ + static @NonNull Pageable afterCursor(@NonNull Cursor cursor, int page, int size, @Nullable Sort sort) { + if (sort == null) { + sort = UNSORTED; + } + return new DefaultCursoredPageable(size, cursor, null, false, page, sort); + } + + /** + * Create a new {@link Pageable} for backward pagination given the cursor after which to query. + * + * @param cursor The cursor + * @param page The page number + * @param size The page size + * @param sort The sorting + * @return The pageable + */ + static @NonNull Pageable beforeCursor(@NonNull Cursor cursor, int page, int size, @Nullable Sort sort) { + if (sort == null) { + sort = UNSORTED; + } + return new DefaultCursoredPageable(size, null, cursor, true, page, sort); + } + + /** + * The type of pagination: offset-based or cursor-based, which includes + * a direction. + */ + enum Mode { + /** + * Indicates forward cursor-based pagination, which follows the + * direction of the sort criteria, using a cursor that is + * formed from the key of the last entity on the current page. + */ + CURSOR_NEXT, + + /** + * Indicates a request for a page with cursor-based pagination + * in the previous page direction to the sort criteria, using a cursor + * that is formed from the key of first entity on the current page. + * The order of results on each page follows the sort criteria + * and is not reversed. + */ + CURSOR_PREVIOUS, + + /** + * Indicates a request for a page using offset pagination. + * The starting position for pages is computed as an offset from + * the first result based on the page number and maximum page size. + * Offset pagination is used when a cursor is not supplied. + */ + OFFSET + } + + /** + * An interface for defining pagination cursors. + * It is generally a list of elements which can be used to create a query for the next + * or previous page. + */ + interface Cursor { + /** + * Returns the cursor element at the specified position. + * @param index The index of the cursor value + * @return The cursor value + */ + Object get(int index); + + /** + * Returns all the cursor values in a list. + * @return The cursor values + */ + List elements(); + + /** + * @return The number of elements in the cursor. + */ + int size(); + + /** + * Create a cursor from elements. + * @param elements The cursor elements + * @return The cursor + */ + static Cursor of(Object... elements) { + return new DefaultCursoredPageable.DefaultCursor(Arrays.asList(elements)); + } + + /** + * Create a cursor from elements. + * @param elements The cursor elements + * @return The cursor + */ + static Cursor of(List elements) { + return new DefaultCursoredPageable.DefaultCursor(elements); + } + } } diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/DefaultSqlPreparedQuery.java b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/DefaultSqlPreparedQuery.java index 698c5de96b0..cc8f96a4b69 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/DefaultSqlPreparedQuery.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/DefaultSqlPreparedQuery.java @@ -22,6 +22,7 @@ import io.micronaut.data.model.CursoredPageable; import io.micronaut.data.model.DataType; import io.micronaut.data.model.Pageable; +import io.micronaut.data.model.Pageable.Cursor; import io.micronaut.data.model.PersistentProperty; import io.micronaut.data.model.Sort; import io.micronaut.data.model.Sort.Order; @@ -245,7 +246,7 @@ private Sort reverseSort(Sort sort) { * @return The additional query part */ @NonNull - private String buildCursorPagination(@Nullable List cursor, @NonNull Sort sort) { + private String buildCursorPagination(@Nullable Pageable.Cursor cursor, @NonNull Sort sort) { List orders = sort.getOrderBy(); cursorProperties = new ArrayList<>(orders.size()); for (Order order: orders) { @@ -317,21 +318,21 @@ public Pageable updatePageable(List results, Pageable pageable, long tot Collections.reverse(results); } - List startCursor = null; - List endCursor = null; + Cursor startCursor = null; + Cursor endCursor = null; if (!results.isEmpty()) { if (!cursored.isBackward() || results.size() == cursored.getSize()) { E firstValue = (E) results.get(0); - startCursor = new ArrayList<>(cursorProperties.size()); + startCursor = Cursor.of(new ArrayList<>(cursorProperties.size())); for (RuntimePersistentProperty property : cursorProperties) { - startCursor.add(property.getProperty().get(firstValue)); + startCursor.elements().add(property.getProperty().get(firstValue)); } } if (cursored.isBackward() || results.size() == cursored.getSize()) { E lastValue = (E) results.get(results.size() - 1); - endCursor = new ArrayList<>(cursorProperties.size()); + endCursor = Cursor.of(new ArrayList<>(cursorProperties.size())); for (RuntimePersistentProperty property : cursorProperties) { - endCursor.add(property.getProperty().get(lastValue)); + endCursor.elements().add(property.getProperty().get(lastValue)); } } } else { From 6167f12a9434f3f08370033df6eb43a60ed675a1 Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Tue, 16 Apr 2024 16:43:18 -0400 Subject: [PATCH 08/20] Add requestTotal property to the pageable --- .../FindPageSpecificationInterceptor.java | 30 +++++----- .../data/model/CursoredPageable.java | 8 +-- .../data/model/DefaultCursoredPageable.java | 29 ++++++++-- .../io/micronaut/data/model/DefaultPage.java | 14 ++++- .../micronaut/data/model/DefaultPageable.java | 25 +++++++- .../java/io/micronaut/data/model/Page.java | 19 +++++- .../io/micronaut/data/model/Pageable.java | 58 ++++++++++--------- .../intercept/DefaultFindPageInterceptor.java | 17 +++--- .../FindPageSpecificationInterceptor.java | 7 ++- ...FindPageAsyncSpecificationInterceptor.java | 11 +++- ...dPageReactiveSpecificationInterceptor.java | 9 ++- .../internal/sql/DefaultSqlPreparedQuery.java | 3 +- .../data/tck/tests/AbstractPageSpec.groovy | 24 ++++++++ 13 files changed, 183 insertions(+), 71 deletions(-) diff --git a/data-jpa/src/main/java/io/micronaut/data/jpa/repository/intercept/FindPageSpecificationInterceptor.java b/data-jpa/src/main/java/io/micronaut/data/jpa/repository/intercept/FindPageSpecificationInterceptor.java index 505c2476045..ad6d1b4b0b6 100644 --- a/data-jpa/src/main/java/io/micronaut/data/jpa/repository/intercept/FindPageSpecificationInterceptor.java +++ b/data-jpa/src/main/java/io/micronaut/data/jpa/repository/intercept/FindPageSpecificationInterceptor.java @@ -96,29 +96,33 @@ public Page intercept(RepositoryMethodKey methodKey, MethodInvocationContext results = typedQuery.getResultList(); - final CriteriaQuery countQuery = criteriaBuilder.createQuery(Long.class); - final Root countRoot = countQuery.from(rootEntity); - final Predicate countPredicate = specification != null ? specification.toPredicate(countRoot, countQuery, criteriaBuilder) : null; - if (countPredicate != null) { - countQuery.where(countPredicate); - } - if (countQuery.isDistinct()) { - countQuery.select(criteriaBuilder.countDistinct(countRoot)); - } else { - countQuery.select(criteriaBuilder.count(countRoot)); + + Long totalCount = null; + if (pageable.requestTotal()) { + final CriteriaQuery countQuery = criteriaBuilder.createQuery(Long.class); + final Root countRoot = countQuery.from(rootEntity); + final Predicate countPredicate = specification != null ? specification.toPredicate(countRoot, countQuery, criteriaBuilder) : null; + if (countPredicate != null) { + countQuery.where(countPredicate); + } + if (countQuery.isDistinct()) { + countQuery.select(criteriaBuilder.countDistinct(countRoot)); + } else { + countQuery.select(criteriaBuilder.count(countRoot)); + } + totalCount = entityManager.createQuery(countQuery).getSingleResult(); } - Long singleResult = entityManager.createQuery(countQuery).getSingleResult(); return Page.of( results, pageable, - singleResult + totalCount ); } diff --git a/data-model/src/main/java/io/micronaut/data/model/CursoredPageable.java b/data-model/src/main/java/io/micronaut/data/model/CursoredPageable.java index a1e4f5a187e..82b0238750d 100644 --- a/data-model/src/main/java/io/micronaut/data/model/CursoredPageable.java +++ b/data-model/src/main/java/io/micronaut/data/model/CursoredPageable.java @@ -41,7 +41,7 @@ public interface CursoredPageable extends Pageable { * Constant for no pagination. */ CursoredPageable UNPAGED = new DefaultCursoredPageable( - -1, null, null, false, 0, Sort.UNSORTED + -1, null, null, false, 0, Sort.UNSORTED, true ); /** @@ -110,7 +110,7 @@ default boolean hasPrevious() { return UNPAGED; } return new DefaultCursoredPageable( - -1, null, null, false, 0, sort + -1, null, null, false, 0, sort, true ); } @@ -128,7 +128,7 @@ default boolean hasPrevious() { if (sort == null) { sort = UNSORTED; } - return new DefaultCursoredPageable(size, null, null, false, 0, sort); + return new DefaultCursoredPageable(size, null, null, false, 0, sort, true); } /** @@ -154,7 +154,7 @@ default boolean hasPrevious() { if (sort == null) { sort = UNSORTED; } - return new DefaultCursoredPageable(size, startCursor, endCursor, isBackward, page, sort); + return new DefaultCursoredPageable(size, startCursor, endCursor, isBackward, page, sort, true); } /** diff --git a/data-model/src/main/java/io/micronaut/data/model/DefaultCursoredPageable.java b/data-model/src/main/java/io/micronaut/data/model/DefaultCursoredPageable.java index fc9b2b7dc7e..01c54877195 100644 --- a/data-model/src/main/java/io/micronaut/data/model/DefaultCursoredPageable.java +++ b/data-model/src/main/java/io/micronaut/data/model/DefaultCursoredPageable.java @@ -37,7 +37,8 @@ record DefaultCursoredPageable( Cursor endCursor, boolean isBackward, int page, - Sort sort + Sort sort, + boolean requestTotal ) implements CursoredPageable { /** @@ -97,12 +98,13 @@ public boolean isBackward() { public CursoredPageable next() { if (endCursor != null) { return new DefaultCursoredPageable( - getSize(), + size, endCursor, null, false, page + 1, - getSort() + sort, + requestTotal ); } return CursoredPageable.super.next(); @@ -112,17 +114,34 @@ public CursoredPageable next() { public CursoredPageable previous() { if (startCursor != null) { return new DefaultCursoredPageable( - getSize(), + size, null, startCursor, true, Math.max(page - 1, 0), - getSort() + sort, + requestTotal ); } return CursoredPageable.super.previous(); } + @Override + public Pageable withTotal() { + if (requestTotal) { + return this; + } + return new DefaultCursoredPageable(size, startCursor, endCursor, isBackward, page, sort, true); + } + + @Override + public Pageable withoutTotal() { + if (!requestTotal) { + return this; + } + return new DefaultCursoredPageable(size, startCursor, endCursor, isBackward, page, sort, true); + } + @Override public boolean hasNext() { return endCursor != null; diff --git a/data-model/src/main/java/io/micronaut/data/model/DefaultPage.java b/data-model/src/main/java/io/micronaut/data/model/DefaultPage.java index a86f3757512..b6373c597bf 100644 --- a/data-model/src/main/java/io/micronaut/data/model/DefaultPage.java +++ b/data-model/src/main/java/io/micronaut/data/model/DefaultPage.java @@ -34,7 +34,7 @@ @Serdeable class DefaultPage extends DefaultSlice implements Page { - private final long totalSize; + private final Long totalSize; /** * Default constructor. @@ -51,14 +51,24 @@ class DefaultPage extends DefaultSlice implements Page { @JsonProperty("pageable") Pageable pageable, @JsonProperty("totalSize") - long totalSize) { + Long totalSize) { super(content, pageable); this.totalSize = totalSize; } + + @Override + public boolean hasTotalSize() { + return totalSize != null; + } + @Override @ReflectiveAccess public long getTotalSize() { + if (totalSize == null) { + throw new IllegalStateException("Page does not contain total count. " + + "It is likely that the Pageable needs to be modified to request this information."); + } return totalSize; } diff --git a/data-model/src/main/java/io/micronaut/data/model/DefaultPageable.java b/data-model/src/main/java/io/micronaut/data/model/DefaultPageable.java index f07d8b52317..01220efd40e 100644 --- a/data-model/src/main/java/io/micronaut/data/model/DefaultPageable.java +++ b/data-model/src/main/java/io/micronaut/data/model/DefaultPageable.java @@ -32,6 +32,7 @@ @Introspected final class DefaultPageable implements Pageable { + private final boolean requestTotal; private final int max; private final int number; private final Sort sort; @@ -44,7 +45,7 @@ final class DefaultPageable implements Pageable { * @param sort The sort */ @Creator - DefaultPageable(int page, int size, @Nullable Sort sort) { + DefaultPageable(int page, int size, @Nullable Sort sort, Boolean requestTotal) { if (page < 0) { throw new IllegalArgumentException("Page index cannot be negative"); } @@ -54,6 +55,7 @@ final class DefaultPageable implements Pageable { this.max = size; this.number = page; this.sort = sort == null ? Sort.unsorted() : sort; + this.requestTotal = requestTotal == null ? true : requestTotal; } @Override @@ -76,12 +78,33 @@ public Optional cursor() { return Optional.empty(); } + @Override + public boolean requestTotal() { + return requestTotal; + } + @NonNull @Override public Sort getSort() { return sort; } + @Override + public Pageable withTotal() { + if (this.requestTotal) { + return this; + } + return new DefaultPageable(number, max, sort, true); + } + + @Override + public Pageable withoutTotal() { + if (!this.requestTotal) { + return this; + } + return new DefaultPageable(number, max, sort, false); + } + @Override public boolean equals(Object o) { if (this == o) { diff --git a/data-model/src/main/java/io/micronaut/data/model/Page.java b/data-model/src/main/java/io/micronaut/data/model/Page.java index 467cc680c1c..5d34218ab13 100644 --- a/data-model/src/main/java/io/micronaut/data/model/Page.java +++ b/data-model/src/main/java/io/micronaut/data/model/Page.java @@ -21,6 +21,7 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import io.micronaut.context.annotation.DefaultImplementation; import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; import io.micronaut.core.annotation.ReflectiveAccess; import io.micronaut.core.annotation.TypeHint; import io.micronaut.serde.annotation.Serdeable; @@ -48,14 +49,28 @@ @DefaultImplementation(DefaultPage.class) public interface Page extends Slice { - Page EMPTY = new DefaultPage<>(Collections.emptyList(), Pageable.unpaged(), 0); + Page EMPTY = new DefaultPage<>(Collections.emptyList(), Pageable.unpaged(), null); /** + * @return Whether this {@link Page} contains the total count of the records + * @since 4.8.0 + */ + boolean hasTotalSize(); + + /** + * Get the total count of all the records that can be given by this query. + * The method may produce a {@link IllegalStateException} if the {@link Pageable} request + * did not ask for total size. + * * @return The total size of the all records. */ long getTotalSize(); /** + * Get the total count of pages that can be given by this query. + * The method may produce a {@link IllegalStateException} if the {@link Pageable} request + * did not ask for total size. + * * @return The total number of pages */ default int getTotalPages() { @@ -115,7 +130,7 @@ default boolean hasPrevious() { static @NonNull Page of( @JsonProperty("content") @NonNull List content, @JsonProperty("pageable") @NonNull Pageable pageable, - @JsonProperty("totalSize") long totalSize) { + @JsonProperty("totalSize") @Nullable Long totalSize) { return new DefaultPage<>(content, pageable, totalSize); } diff --git a/data-model/src/main/java/io/micronaut/data/model/Pageable.java b/data-model/src/main/java/io/micronaut/data/model/Pageable.java index 893f4f57383..e041bb5ca43 100644 --- a/data-model/src/main/java/io/micronaut/data/model/Pageable.java +++ b/data-model/src/main/java/io/micronaut/data/model/Pageable.java @@ -43,27 +43,7 @@ public interface Pageable extends Sort { /** * Constant for no pagination. */ - Pageable UNPAGED = new Pageable() { - @Override - public int getNumber() { - return 0; - } - - @Override - public Mode getMode() { - return Mode.OFFSET; - } - - @Override - public Optional cursor() { - return Optional.empty(); - } - - @Override - public int getSize() { - return -1; - } - }; + Pageable UNPAGED = new DefaultPageable(0, -1, Sort.UNSORTED, true); /** * @return The page number. @@ -89,6 +69,16 @@ public int getSize() { */ Optional cursor(); + /** + * Whether the returned page should contain information about total items that + * can be produced by this query. If the value is false, {@link Page#getTotalSize()} and + * {@link Page#getTotalPages()} methods will fail. By default, pageable will have this value + * set to true. + * + * @return Whether total size information is required. + */ + boolean requestTotal(); + /** * Offset in the requested collection. Defaults to zero. * @return offset in the requested collection @@ -188,13 +178,27 @@ default List getOrderBy() { return getSort().getOrderBy(); } + /** + * Specify that the {@link Page} response should have information about total size. + * @see #requestTotal() requestTotal() for more details. + * @return A pageable instance that will request the total size. + */ + Pageable withTotal(); + + /** + * Specify that the {@link Page} response should not have information about total size. + * @see #requestTotal() requestTotal() for more details. + * @return A pageable instance that won't request the total size. + */ + Pageable withoutTotal(); + /** * Creates a new {@link Pageable} at the given offset with a default size of 10. * @param page The page * @return The pageable */ static @NonNull Pageable from(int page) { - return new DefaultPageable(page, 10, null); + return new DefaultPageable(page, 10, null, true); } /** @@ -204,7 +208,7 @@ default List getOrderBy() { * @return The pageable */ static @NonNull Pageable from(int page, int size) { - return new DefaultPageable(page, size, null); + return new DefaultPageable(page, size, null, true); } /** @@ -219,7 +223,7 @@ default List getOrderBy() { @JsonProperty("number") int page, @JsonProperty("size") int size, @JsonProperty("sort") @Nullable Sort sort) { - return new DefaultPageable(page, size, sort); + return new DefaultPageable(page, size, sort, true); } /** @@ -231,7 +235,7 @@ default List getOrderBy() { if (sort == null) { return UNPAGED; } else { - return new DefaultPageable(0, -1, sort); + return new DefaultPageable(0, -1, sort, true); } } @@ -255,7 +259,7 @@ default List getOrderBy() { if (sort == null) { sort = UNSORTED; } - return new DefaultCursoredPageable(size, cursor, null, false, page, sort); + return new DefaultCursoredPageable(size, cursor, null, false, page, sort, true); } /** @@ -271,7 +275,7 @@ default List getOrderBy() { if (sort == null) { sort = UNSORTED; } - return new DefaultCursoredPageable(size, null, cursor, true, page, sort); + return new DefaultCursoredPageable(size, null, cursor, true, page, sort, true); } /** diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/DefaultFindPageInterceptor.java b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/DefaultFindPageInterceptor.java index fc0bd23a8ce..e6acc4bbc77 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/DefaultFindPageInterceptor.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/DefaultFindPageInterceptor.java @@ -52,19 +52,22 @@ public R intercept(RepositoryMethodKey methodKey, MethodInvocationContext Class returnType = context.getReturnType().getType(); if (context.hasAnnotation(Query.class)) { PreparedQuery preparedQuery = prepareQuery(methodKey, context); - PreparedQuery countQuery = prepareCountQuery(methodKey, context); Iterable iterable = operations.findAll(preparedQuery); - List resultList = (List) CollectionUtils.iterableToList(iterable); - Number n = operations.findOne(countQuery); - Long result = n != null ? n.longValue() : 0; - + List results = (List) CollectionUtils.iterableToList(iterable); Pageable pageable = getPageable(context); if (preparedQuery instanceof DefaultSqlPreparedQuery sqlPreparedQuery) { - pageable = sqlPreparedQuery.updatePageable(resultList, pageable, result); + pageable = sqlPreparedQuery.updatePageable(results, pageable); + } + + Long totalCount = null; + if (pageable.requestTotal()) { + PreparedQuery countQuery = prepareCountQuery(methodKey, context); + Number n = operations.findOne(countQuery); + totalCount = n != null ? n.longValue() : null; } - Page page = Page.of(resultList, pageable, result); + Page page = Page.of(results, pageable, totalCount); if (returnType.isInstance(page)) { return (R) page; } else { diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/criteria/FindPageSpecificationInterceptor.java b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/criteria/FindPageSpecificationInterceptor.java index a07ee0dbcb9..36419aa0c0b 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/criteria/FindPageSpecificationInterceptor.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/criteria/FindPageSpecificationInterceptor.java @@ -56,14 +56,17 @@ public Object intercept(RepositoryMethodKey methodKey, MethodInvocationContext iterable = findAll(methodKey, context, Type.FIND_PAGE); List resultList = (List) CollectionUtils.iterableToList(iterable); - Long count = count(methodKey, context); + Long count = null; + if (pageable.requestTotal()) { + count = count(methodKey, context); + } Page page = Page.of(resultList, getPageable(context), count); Class rt = context.getReturnType().getType(); diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/criteria/async/FindPageAsyncSpecificationInterceptor.java b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/criteria/async/FindPageAsyncSpecificationInterceptor.java index 38375370a7a..6900ba68131 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/criteria/async/FindPageAsyncSpecificationInterceptor.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/criteria/async/FindPageAsyncSpecificationInterceptor.java @@ -24,6 +24,8 @@ import io.micronaut.data.operations.RepositoryOperations; import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; /** * Runtime implementation of {@code CompletableFuture find(Specification, Pageable)}. @@ -57,11 +59,14 @@ public Object intercept(RepositoryMethodKey methodKey, MethodInvocationContext { List resultList = CollectionUtils.iterableToList(iterable); - return Page.of(resultList, pageable, resultList.size()); + return Page.of(resultList, pageable, (long) resultList.size()); }); } - return findAllAsync(methodKey, context, Type.FIND_PAGE).thenCompose(iterable -> countAsync(methodKey, context) - .thenApply(count -> Page.of(CollectionUtils.iterableToList(iterable), pageable, count.longValue()))); + return findAllAsync(methodKey, context, Type.FIND_PAGE).thenCompose(iterable -> + pageable.requestTotal() + ? countAsync(methodKey, context).thenApply(count -> Page.of(CollectionUtils.iterableToList(iterable), pageable, count.longValue())) + : CompletableFuture.completedFuture(Page.of(CollectionUtils.iterableToList(iterable), pageable, null)) + ); } diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/criteria/reactive/FindPageReactiveSpecificationInterceptor.java b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/criteria/reactive/FindPageReactiveSpecificationInterceptor.java index aeb7de6682e..c2667321db3 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/criteria/reactive/FindPageReactiveSpecificationInterceptor.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/criteria/reactive/FindPageReactiveSpecificationInterceptor.java @@ -55,12 +55,15 @@ public Object intercept(RepositoryMethodKey methodKey, MethodInvocationContext results = Flux.from(findAllReactive(methodKey, context, Type.FIND_PAGE)); - result = results.collectList().map(resultList -> Page.of(resultList, pageable, resultList.size())); + result = results.collectList().map(resultList -> Page.of(resultList, pageable, (long) resultList.size())); } else { result = Flux.from(findAllReactive(methodKey, context, Type.FIND_PAGE)) .collectList() - .flatMap(list -> Mono.from(countReactive(methodKey, context)) - .map(count -> Page.of(list, getPageable(context), count.longValue()))); + .flatMap( + list -> pageable.requestTotal() + ? Mono.from(countReactive(methodKey, context)).map(count -> Page.of(list, getPageable(context), count)) + : Mono.just(Page.of(list, getPageable(context), null)) + ); } return Publishers.convertPublisher(conversionService, result, context.getReturnType().getType()); diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/DefaultSqlPreparedQuery.java b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/DefaultSqlPreparedQuery.java index cc8f96a4b69..cc828889ba0 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/DefaultSqlPreparedQuery.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/DefaultSqlPreparedQuery.java @@ -309,10 +309,9 @@ private String buildCursorPagination(@Nullable Pageable.Cursor cursor, @NonNull * * @param results The scanning results * @param pageable The pageable sent by user - * @param totalSize The total count * @return The updated pageable */ - public Pageable updatePageable(List results, Pageable pageable, long totalSize) { + public Pageable updatePageable(List results, Pageable pageable) { if (pageable instanceof CursoredPageable cursored) { if (cursored.isBackward()) { Collections.reverse(results); diff --git a/data-tck/src/main/groovy/io/micronaut/data/tck/tests/AbstractPageSpec.groovy b/data-tck/src/main/groovy/io/micronaut/data/tck/tests/AbstractPageSpec.groovy index 282400f0ceb..8cf2f8c588c 100644 --- a/data-tck/src/main/groovy/io/micronaut/data/tck/tests/AbstractPageSpec.groovy +++ b/data-tck/src/main/groovy/io/micronaut/data/tck/tests/AbstractPageSpec.groovy @@ -82,6 +82,7 @@ abstract class AbstractPageSpec extends Specification { page.nextPageable().offset == 10 page.nextPageable().size == 10 page.hasNext() + page.hasTotalSize() !page.hasPrevious() when: "The next page is selected" @@ -109,6 +110,29 @@ abstract class AbstractPageSpec extends Specification { !page.hasPrevious() } + void "test pageable list without total count"() { + when: "10 people are paged" + def pageable = Pageable.from(0, 10).withoutTotal() + Page page = personRepository.findAll(pageable) + + then: "The data is correct" + page.content.size() == 10 + page.content.every() { it instanceof Person } + !page.hasTotalSize() + + when: + page.getTotalPages() + + then: + thrown(IllegalStateException) + + when: + page.getTotalSize() + + then: + thrown(IllegalStateException) + } + void "test pageable sort"() { when: "All the people are count" def count = personRepository.count() From 17f81d356dad874280847a4a2b4c6571542c7744 Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Tue, 16 Apr 2024 16:52:08 -0400 Subject: [PATCH 09/20] Update Page to account for cases when total size is not queried --- data-model/src/main/java/io/micronaut/data/model/Page.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/data-model/src/main/java/io/micronaut/data/model/Page.java b/data-model/src/main/java/io/micronaut/data/model/Page.java index 5d34218ab13..a13a29ba662 100644 --- a/data-model/src/main/java/io/micronaut/data/model/Page.java +++ b/data-model/src/main/java/io/micronaut/data/model/Page.java @@ -88,7 +88,9 @@ default boolean hasNext() { if (getPageable() instanceof CursoredPageable cursoredPageable) { return cursoredPageable.hasNext(); } - return getOffset() + getSize() < getTotalSize(); + return hasTotalSize() + ? getOffset() + getSize() < getTotalSize() + : getContent().size() == getSize(); } /** From b9aa45924e5af2e3b86c937784dfbef0a099e886 Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Tue, 16 Apr 2024 17:11:26 -0400 Subject: [PATCH 10/20] Fix build --- config/checkstyle/suppressions.xml | 2 ++ ...ctiveFindPageSpecificationInterceptor.java | 6 ++++- .../data/model/DefaultCursoredPageable.java | 19 ++++++------- .../io/micronaut/data/model/DefaultPage.java | 1 - .../model/runtime/QueryParameterBinding.java | 2 +- .../AbstractSpecificationInterceptor.java | 2 +- ...FindPageAsyncSpecificationInterceptor.java | 1 - .../data/spring/runtime/PageableDelegate.java | 27 +++++++++++++++++++ 8 files changed, 46 insertions(+), 14 deletions(-) diff --git a/config/checkstyle/suppressions.xml b/config/checkstyle/suppressions.xml index 73f71b3a499..dd6354e9969 100644 --- a/config/checkstyle/suppressions.xml +++ b/config/checkstyle/suppressions.xml @@ -9,4 +9,6 @@ + + diff --git a/data-hibernate-reactive/src/main/java/io/micronaut/data/hibernate/reactive/repository/jpa/intercept/ReactiveFindPageSpecificationInterceptor.java b/data-hibernate-reactive/src/main/java/io/micronaut/data/hibernate/reactive/repository/jpa/intercept/ReactiveFindPageSpecificationInterceptor.java index 4142a959c90..847516041c1 100644 --- a/data-hibernate-reactive/src/main/java/io/micronaut/data/hibernate/reactive/repository/jpa/intercept/ReactiveFindPageSpecificationInterceptor.java +++ b/data-hibernate-reactive/src/main/java/io/micronaut/data/hibernate/reactive/repository/jpa/intercept/ReactiveFindPageSpecificationInterceptor.java @@ -84,7 +84,7 @@ protected Publisher interceptPublisher(RepositoryMethodKey methodKey, MethodI return operations.withSession(session -> { if (pageable.isUnpaged()) { return Mono.fromCompletionStage(() -> session.createQuery(query).getResultList()) - .map(resultList -> Page.of(resultList, pageable, resultList.size())); + .map(resultList -> Page.of(resultList, pageable, (long) resultList.size())); } return Mono.fromCompletionStage(() -> { Stage.SelectionQuery q = session.createQuery(query); @@ -92,6 +92,10 @@ protected Publisher interceptPublisher(RepositoryMethodKey methodKey, MethodI q.setMaxResults(pageable.getSize()); return q.getResultList(); }).flatMap(results -> { + if (!pageable.requestTotal()) { + return Mono.just(Page.of(results, pageable, null)); + } + final CriteriaQuery countQuery = criteriaBuilder.createQuery(Long.class); final Root countRoot = countQuery.from(rootEntity); final Predicate countPredicate = specification != null ? specification.toPredicate(countRoot, countQuery, criteriaBuilder) : null; diff --git a/data-model/src/main/java/io/micronaut/data/model/DefaultCursoredPageable.java b/data-model/src/main/java/io/micronaut/data/model/DefaultCursoredPageable.java index 01c54877195..269d8f7acab 100644 --- a/data-model/src/main/java/io/micronaut/data/model/DefaultCursoredPageable.java +++ b/data-model/src/main/java/io/micronaut/data/model/DefaultCursoredPageable.java @@ -25,6 +25,16 @@ /** * The default cursored pageable implementation. * + * @param page The page. + * @param startCursor The cursor that is pointing to the start of the data. + * This cursor will be used for forward pagination. + * @param endCursor The cursor that is pointing to the end of the data. + * This cursor will be used for backward pagination + * @param isBackward Whether user requested for backward pagination. + * @param size The size of a page + * @param sort The sorting + * @param requestTotal Whether to request the total count + * * @author Andriy Dmytruk * @since 4.8.0 */ @@ -43,15 +53,6 @@ record DefaultCursoredPageable( /** * Default constructor. - * - * @param page The page. - * @param startCursor The cursor that is pointing to the start of the data. - * This cursor will be used for forward pagination. - * @param endCursor The cursor that is pointing to the end of the data. - * This cursor will be used for backward pagination - * @param isBackward Whether user requested for backward pagination. - * @param size The size of a page - * @param sort The sorting */ @Creator DefaultCursoredPageable { diff --git a/data-model/src/main/java/io/micronaut/data/model/DefaultPage.java b/data-model/src/main/java/io/micronaut/data/model/DefaultPage.java index b6373c597bf..1845a899b6c 100644 --- a/data-model/src/main/java/io/micronaut/data/model/DefaultPage.java +++ b/data-model/src/main/java/io/micronaut/data/model/DefaultPage.java @@ -56,7 +56,6 @@ class DefaultPage extends DefaultSlice implements Page { this.totalSize = totalSize; } - @Override public boolean hasTotalSize() { return totalSize != null; diff --git a/data-model/src/main/java/io/micronaut/data/model/runtime/QueryParameterBinding.java b/data-model/src/main/java/io/micronaut/data/model/runtime/QueryParameterBinding.java index d36035fe5f4..84453683ef7 100644 --- a/data-model/src/main/java/io/micronaut/data/model/runtime/QueryParameterBinding.java +++ b/data-model/src/main/java/io/micronaut/data/model/runtime/QueryParameterBinding.java @@ -150,7 +150,7 @@ default Object getValue() { /** * @return Is expression value - * @see 4.5.0 + * @since 4.5.0 */ default boolean isExpression() { return false; diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/criteria/AbstractSpecificationInterceptor.java b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/criteria/AbstractSpecificationInterceptor.java index e6ae8060965..447b0726ffd 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/criteria/AbstractSpecificationInterceptor.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/criteria/AbstractSpecificationInterceptor.java @@ -167,7 +167,7 @@ protected final Long count(RepositoryMethodKey methodKey, MethodInvocationContex Set methodJoinPaths = getMethodJoinPaths(methodKey, context); Long count; if (criteriaRepositoryOperations != null) { - count = criteriaRepositoryOperations.findOne(buildCountQuery(context)); + count = criteriaRepositoryOperations.findOne(buildCountQuery(context)); } else { count = operations.findOne(preparedQueryForCriteria(methodKey, context, Type.COUNT, methodJoinPaths)); } diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/criteria/async/FindPageAsyncSpecificationInterceptor.java b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/criteria/async/FindPageAsyncSpecificationInterceptor.java index 6900ba68131..0b59ad33e7e 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/criteria/async/FindPageAsyncSpecificationInterceptor.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/criteria/async/FindPageAsyncSpecificationInterceptor.java @@ -25,7 +25,6 @@ import java.util.List; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; /** * Runtime implementation of {@code CompletableFuture find(Specification, Pageable)}. diff --git a/data-spring/src/main/java/io/micronaut/data/spring/runtime/PageableDelegate.java b/data-spring/src/main/java/io/micronaut/data/spring/runtime/PageableDelegate.java index 9a44d6799ab..f740970a5e8 100644 --- a/data-spring/src/main/java/io/micronaut/data/spring/runtime/PageableDelegate.java +++ b/data-spring/src/main/java/io/micronaut/data/spring/runtime/PageableDelegate.java @@ -20,6 +20,8 @@ import io.micronaut.data.model.Pageable; import io.micronaut.data.model.Sort; +import java.util.Optional; + /** * Supports representing a Spring Pageable as a Micronaut {@link Pageable}. * @@ -49,6 +51,21 @@ public int getSize() { return target.getPageSize(); } + @Override + public Mode getMode() { + return Mode.OFFSET; + } + + @Override + public Optional cursor() { + return Optional.empty(); + } + + @Override + public boolean requestTotal() { + return true; + } + @Override public long getOffset() { return target.getOffset(); @@ -59,4 +76,14 @@ public long getOffset() { public Sort getSort() { return new SortDelegate(target.getSort()); } + + @Override + public Pageable withTotal() { + return this; + } + + @Override + public Pageable withoutTotal() { + throw new IllegalStateException("Disabling requesting total is not supported for current Pageable"); + } } From 5146d907363e804f45f7e6908706e9844dfd7b3c Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Wed, 17 Apr 2024 13:29:33 -0400 Subject: [PATCH 11/20] Implement more review comments --- .../DefaultJdbcRepositoryOperations.java | 2 +- .../data/model/CursoredPageable.java | 94 +++------- .../data/model/DefaultCursoredPageable.java | 103 +++++++---- .../micronaut/data/model/DefaultPageable.java | 11 +- .../java/io/micronaut/data/model/Page.java | 15 +- .../io/micronaut/data/model/Pageable.java | 172 +++++++++++++----- .../java/io/micronaut/data/model/Slice.java | 4 +- .../java/io/micronaut/data/model/Sort.java | 13 ++ .../query/builder/sql/SqlQueryBuilder.java | 3 +- .../io/micronaut/data/model/PageSpec.groovy | 31 +++- .../DefaultFindSliceInterceptor.java | 9 +- .../internal/sql/DefaultSqlPreparedQuery.java | 30 +-- 12 files changed, 292 insertions(+), 195 deletions(-) diff --git a/data-jdbc/src/main/java/io/micronaut/data/jdbc/operations/DefaultJdbcRepositoryOperations.java b/data-jdbc/src/main/java/io/micronaut/data/jdbc/operations/DefaultJdbcRepositoryOperations.java index 98215e7240f..baf4cb083ec 100644 --- a/data-jdbc/src/main/java/io/micronaut/data/jdbc/operations/DefaultJdbcRepositoryOperations.java +++ b/data-jdbc/src/main/java/io/micronaut/data/jdbc/operations/DefaultJdbcRepositoryOperations.java @@ -726,7 +726,7 @@ public Stream findStream(@NonNull PagedQuery query) { @Override public Page findPage(@NonNull PagedQuery query) { - throw new UnsupportedOperationException("The findPage method without an explicit query is not supported. Use findPage(PreparedQuery) instead"); + throw new UnsupportedOperationException("The findPage method without an explicit query is not supported. Use findAll(PreparedQuery) instead"); } @Override diff --git a/data-model/src/main/java/io/micronaut/data/model/CursoredPageable.java b/data-model/src/main/java/io/micronaut/data/model/CursoredPageable.java index 82b0238750d..153d481a0f6 100644 --- a/data-model/src/main/java/io/micronaut/data/model/CursoredPageable.java +++ b/data-model/src/main/java/io/micronaut/data/model/CursoredPageable.java @@ -18,16 +18,14 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; +import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.Introspected; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.serde.annotation.Serdeable; -import java.util.List; -import java.util.Optional; - /** - * Models pageable data that uses a cursor. + * Models pageable data that uses a currentCursor. * * @author Andriy Dmytruk * @since 4.8.0 @@ -41,27 +39,13 @@ public interface CursoredPageable extends Pageable { * Constant for no pagination. */ CursoredPageable UNPAGED = new DefaultCursoredPageable( - -1, null, null, false, 0, Sort.UNSORTED, true + -1, null, null, Mode.CURSOR_NEXT, 0, Sort.UNSORTED, true ); - /** - * @return The cursor values corresponding to the beginning of queried data. - * This cursor is used for forward pagination. - */ - @Nullable - Cursor getStartCursor(); - - /** - * @return The cursor values corresponding to the end of queried data. - * This cursor is used for backward pagination. - */ - @Nullable - Cursor getEndCursor(); - /** * Whether the pageable is traversing backwards. * - * @return Whether cursor is going in reverse direction. + * @return Whether currentCursor is going in reverse direction. */ boolean isBackward(); @@ -70,33 +54,14 @@ default Mode getMode() { return isBackward() ? Mode.CURSOR_PREVIOUS : Mode.CURSOR_NEXT; } - @Override - default Optional cursor() { - return isBackward() ? Optional.ofNullable(getEndCursor()) : Optional.ofNullable(getStartCursor()); - } - @Override default @NonNull CursoredPageable next() { - throw new IllegalStateException("To get the next CursoredPageable, you must retrieve this one from a page"); + throw new IllegalStateException("Cannot retrieve next page, as a currentCursor for that is not present"); } @Override default @NonNull CursoredPageable previous() { - throw new IllegalStateException("To get the next CursoredPageable, you must retrieve this one from a page"); - } - - /** - * @return Whether there is a next page - */ - default boolean hasNext() { - return false; - } - - /** - * @return Whether there is a previous page. - */ - default boolean hasPrevious() { - return false; + throw new IllegalStateException("Cannot retrieve previous page, as a currentCursor for that is not present"); } /** @@ -110,7 +75,7 @@ default boolean hasPrevious() { return UNPAGED; } return new DefaultCursoredPageable( - -1, null, null, false, 0, sort, true + -1, null, null, Mode.CURSOR_NEXT, 0, sort, true ); } @@ -128,33 +93,36 @@ default boolean hasPrevious() { if (sort == null) { sort = UNSORTED; } - return new DefaultCursoredPageable(size, null, null, false, 0, sort, true); + return new DefaultCursoredPageable(size, null, null, Mode.CURSOR_NEXT, 0, sort, true); } /** - * Creates a new {@link CursoredPageable} with the given cursor. + * Creates a new {@link CursoredPageable} with the given currentCursor. * * @param page The page - * @param startCursor The cursor pointing to the beginning of the traversed data. - * @param endCursor The cursor pointing to the end the traversed data. - * @param isBackward Whether the cursor is for backward traversing + * @param cursor The current currentCursor that will be used for querying data. + * @param nextCursor The currentCursor that could be used for querying the next page of data. + * @param mode The pagination mode. Must be either forward or backward currentCursor pagination. * @param size The page size * @param sort The sort + * @param requestTotal Whether to request total count * @return The pageable */ + @Internal @JsonCreator static @NonNull CursoredPageable from( - @JsonProperty("number") int page, - @JsonProperty("startCursor") @Nullable Cursor startCursor, - @JsonProperty("endCursor") @Nullable Cursor endCursor, - @JsonProperty(value = "isBackward", defaultValue = "false") boolean isBackward, - @JsonProperty("size") int size, - @JsonProperty("sort") @Nullable Sort sort + @JsonProperty("page") int page, + @Nullable Cursor cursor, + @Nullable Cursor nextCursor, + Pageable.Mode mode, + int size, + @Nullable Sort sort, + boolean requestTotal ) { if (sort == null) { sort = UNSORTED; } - return new DefaultCursoredPageable(size, startCursor, endCursor, isBackward, page, sort, true); + return new DefaultCursoredPageable(size, cursor, nextCursor, mode, page, sort, requestTotal); } /** @@ -164,22 +132,4 @@ default boolean hasPrevious() { return UNPAGED; } - /** - * Default implementation of the {@link Cursor}. - * - * @param elements The cursor elements - */ - record DefaultCursor( - List elements - ) implements Cursor { - @Override - public Object get(int index) { - return elements.get(index); - } - - @Override - public int size() { - return elements.size(); - } - } } diff --git a/data-model/src/main/java/io/micronaut/data/model/DefaultCursoredPageable.java b/data-model/src/main/java/io/micronaut/data/model/DefaultCursoredPageable.java index 269d8f7acab..0f4dd8bbabd 100644 --- a/data-model/src/main/java/io/micronaut/data/model/DefaultCursoredPageable.java +++ b/data-model/src/main/java/io/micronaut/data/model/DefaultCursoredPageable.java @@ -15,22 +15,26 @@ */ package io.micronaut.data.model; +import com.fasterxml.jackson.annotation.JsonProperty; import io.micronaut.core.annotation.Creator; -import io.micronaut.core.annotation.Introspected; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; +import io.micronaut.serde.annotation.Serdeable; +import java.util.List; import java.util.Objects; +import java.util.Optional; /** * The default cursored pageable implementation. * * @param page The page. - * @param startCursor The cursor that is pointing to the start of the data. - * This cursor will be used for forward pagination. - * @param endCursor The cursor that is pointing to the end of the data. - * This cursor will be used for backward pagination - * @param isBackward Whether user requested for backward pagination. + * @param currentCursor The current currentCursor. This is the currentCursor that will be used for pagination + * in case this pageable is used in a query. + * @param nextCursor A currentCursor for the next page of data. It is stored in pageable to correctly + * support {@link #next()} for forward or {@link #previous()} for backward + * pagination. + * @param mode The pagination mode. Could be one of {@link Mode#CURSOR_NEXT} or {@link Mode#CURSOR_PREVIOUS}. * @param size The size of a page * @param sort The sorting * @param requestTotal Whether to request the total count @@ -38,15 +42,16 @@ * @author Andriy Dmytruk * @since 4.8.0 */ -@Introspected +@Serdeable record DefaultCursoredPageable( int size, @Nullable - Cursor startCursor, + @JsonProperty("cursor") + Cursor currentCursor, @Nullable - Cursor endCursor, - boolean isBackward, - int page, + Cursor nextCursor, + Mode mode, + @JsonProperty("number") int page, Sort sort, boolean requestTotal ) implements CursoredPageable { @@ -62,6 +67,9 @@ record DefaultCursoredPageable( if (size == 0) { throw new IllegalArgumentException("Size cannot be 0"); } + if (mode != Mode.CURSOR_NEXT && mode != Mode.CURSOR_PREVIOUS) { + throw new IllegalArgumentException("The pagination mode must be either currentCursor forward or currentCursor backward"); + } } @Override @@ -69,6 +77,11 @@ public int getSize() { return size; } + @Override + public Optional cursor() { + return Optional.ofNullable(currentCursor); + } + @Override public int getNumber() { return page; @@ -80,29 +93,20 @@ public Sort getSort() { return sort; } - @Override - public Cursor getStartCursor() { - return startCursor; - } - - @Override - public Cursor getEndCursor() { - return endCursor; - } - @Override public boolean isBackward() { - return isBackward; + return mode == Mode.CURSOR_PREVIOUS; } @Override public CursoredPageable next() { - if (endCursor != null) { + Cursor requiredCursor = mode == Mode.CURSOR_PREVIOUS ? currentCursor : nextCursor; + if (requiredCursor != null) { return new DefaultCursoredPageable( size, - endCursor, + requiredCursor, null, - false, + Mode.CURSOR_NEXT, page + 1, sort, requestTotal @@ -113,12 +117,13 @@ public CursoredPageable next() { @Override public CursoredPageable previous() { - if (startCursor != null) { + Cursor requiredCursor = mode == Mode.CURSOR_NEXT ? nextCursor : currentCursor; + if (requiredCursor != null) { return new DefaultCursoredPageable( size, null, - startCursor, - true, + requiredCursor, + Mode.CURSOR_PREVIOUS, Math.max(page - 1, 0), sort, requestTotal @@ -132,7 +137,7 @@ public Pageable withTotal() { if (requestTotal) { return this; } - return new DefaultCursoredPageable(size, startCursor, endCursor, isBackward, page, sort, true); + return new DefaultCursoredPageable(size, currentCursor, nextCursor, mode, page, sort, true); } @Override @@ -140,17 +145,17 @@ public Pageable withoutTotal() { if (!requestTotal) { return this; } - return new DefaultCursoredPageable(size, startCursor, endCursor, isBackward, page, sort, true); + return new DefaultCursoredPageable(size, currentCursor, nextCursor, mode, page, sort, true); } @Override public boolean hasNext() { - return endCursor != null; + return mode == Mode.CURSOR_PREVIOUS ? currentCursor != null : nextCursor != null; } @Override public boolean hasPrevious() { - return startCursor != null; + return mode == Mode.CURSOR_PREVIOUS ? nextCursor != null : currentCursor != null; } @Override @@ -162,24 +167,46 @@ public boolean equals(Object o) { return false; } return size == that.size - && Objects.equals(startCursor, that.startCursor) - && Objects.equals(endCursor, that.endCursor) + && Objects.equals(currentCursor, that.currentCursor) + && Objects.equals(nextCursor, that.nextCursor) + && Objects.equals(mode, that.mode) && Objects.equals(sort, that.sort); } @Override public int hashCode() { - return Objects.hash(size, startCursor, endCursor, sort); + return Objects.hash(size, currentCursor, nextCursor, mode, sort); } @Override public String toString() { return "DefaultCursoredPageable{" + "size=" + size + - ", number=" + page + - ", startCursor=" + startCursor + - ", endCursor=" + endCursor + + ", page=" + page + + ", currentCursor=" + currentCursor + + ", nextCursor=" + nextCursor + + ", mode=" + mode + ", sort=" + sort + '}'; } + + /** + * Default implementation of the {@link Cursor}. + * + * @param elements The currentCursor elements + */ + @Serdeable + record DefaultCursor( + List elements + ) implements Cursor { + @Override + public Object get(int index) { + return elements.get(index); + } + + @Override + public int size() { + return elements.size(); + } + } } diff --git a/data-model/src/main/java/io/micronaut/data/model/DefaultPageable.java b/data-model/src/main/java/io/micronaut/data/model/DefaultPageable.java index 01220efd40e..383335cf56c 100644 --- a/data-model/src/main/java/io/micronaut/data/model/DefaultPageable.java +++ b/data-model/src/main/java/io/micronaut/data/model/DefaultPageable.java @@ -45,7 +45,7 @@ final class DefaultPageable implements Pageable { * @param sort The sort */ @Creator - DefaultPageable(int page, int size, @Nullable Sort sort, Boolean requestTotal) { + DefaultPageable(int page, int size, @Nullable Sort sort, @Nullable Boolean requestTotal) { if (page < 0) { throw new IllegalArgumentException("Page index cannot be negative"); } @@ -55,7 +55,7 @@ final class DefaultPageable implements Pageable { this.max = size; this.number = page; this.sort = sort == null ? Sort.unsorted() : sort; - this.requestTotal = requestTotal == null ? true : requestTotal; + this.requestTotal = requestTotal == null || requestTotal; } @Override @@ -68,11 +68,6 @@ public int getNumber() { return number; } - @Override - public Mode getMode() { - return Mode.OFFSET; - } - @Override public Optional cursor() { return Optional.empty(); @@ -127,7 +122,7 @@ public int hashCode() { public String toString() { return "DefaultPageable{" + "max=" + max + - ", number=" + number + + ", page=" + number + ", sort=" + sort + '}'; } diff --git a/data-model/src/main/java/io/micronaut/data/model/Page.java b/data-model/src/main/java/io/micronaut/data/model/Page.java index a13a29ba662..373434e76d3 100644 --- a/data-model/src/main/java/io/micronaut/data/model/Page.java +++ b/data-model/src/main/java/io/micronaut/data/model/Page.java @@ -24,6 +24,7 @@ import io.micronaut.core.annotation.Nullable; import io.micronaut.core.annotation.ReflectiveAccess; import io.micronaut.core.annotation.TypeHint; +import io.micronaut.data.model.Pageable.Mode; import io.micronaut.serde.annotation.Serdeable; import java.util.Collections; @@ -36,7 +37,7 @@ * pagination operations. * *

A Page is a result set associated with a particular {@link Pageable} that includes a calculation of the total - * size of number of records.

+ * size of page of records.

* * @param The generic type * @author graemerocher @@ -71,7 +72,7 @@ public interface Page extends Slice { * The method may produce a {@link IllegalStateException} if the {@link Pageable} request * did not ask for total size. * - * @return The total number of pages + * @return The total page of pages */ default int getTotalPages() { int size = getSize(); @@ -85,12 +86,12 @@ default int getTotalPages() { * @return Whether there exist a next page. */ default boolean hasNext() { - if (getPageable() instanceof CursoredPageable cursoredPageable) { - return cursoredPageable.hasNext(); + if (getPageable().getMode() == Mode.OFFSET) { + return hasTotalSize() + ? getOffset() + getSize() < getTotalSize() + : getContent().size() == getSize(); } - return hasTotalSize() - ? getOffset() + getSize() < getTotalSize() - : getContent().size() == getSize(); + return getPageable().hasNext(); } /** diff --git a/data-model/src/main/java/io/micronaut/data/model/Pageable.java b/data-model/src/main/java/io/micronaut/data/model/Pageable.java index e041bb5ca43..34f7ba5bac4 100644 --- a/data-model/src/main/java/io/micronaut/data/model/Pageable.java +++ b/data-model/src/main/java/io/micronaut/data/model/Pageable.java @@ -19,6 +19,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; +import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.Introspected; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; @@ -46,7 +47,7 @@ public interface Pageable extends Sort { Pageable UNPAGED = new DefaultPageable(0, -1, Sort.UNSORTED, true); /** - * @return The page number. + * @return The page page. */ int getNumber(); @@ -57,17 +58,25 @@ public interface Pageable extends Sort { int getSize(); /** - * The pagination mode that is either offset pagination, cursor forward or cursor backward + * The pagination mode that is either offset pagination, currentCursor forward or currentCursor backward * pagination. + * + * @since 4.8.0 * @return The pagination mode */ - Mode getMode(); + default Mode getMode() { + return Mode.OFFSET; + } /** - * Get the cursor in case cursored pagination is used. - * @return The cursor + * Get the currentCursor in case cursored pagination is used. + * + * @since 4.8.0 + * @return The currentCursor */ - Optional cursor(); + default Optional cursor() { + return Optional.empty(); + } /** * Whether the returned page should contain information about total items that @@ -75,9 +84,34 @@ public interface Pageable extends Sort { * {@link Page#getTotalPages()} methods will fail. By default, pageable will have this value * set to true. * + * @since 4.8.0 * @return Whether total size information is required. */ - boolean requestTotal(); + default boolean requestTotal() { + return true; + } + + /** + * Return whether there is a next page as best this pageable could determine. + * Use {@link Page} for more reliable results. + * + * @since 4.8.0 + * @return Whether there is a next page + */ + default boolean hasNext() { + return false; + } + + /** + * Return whether there is a previous page as best this pageable could determine. + * Use {@link Page} for more reliable results. + * + * @since 4.8.0 + * @return Whether there is a previous page. + */ + default boolean hasPrevious() { + return false; + } /** * Offset in the requested collection. Defaults to zero. @@ -137,7 +171,7 @@ default Sort getSort() { } /** - * @return Is unpaged + * @return Whether it is unpaged */ @JsonIgnore default boolean isUnpaged() { @@ -180,17 +214,31 @@ default List getOrderBy() { /** * Specify that the {@link Page} response should have information about total size. + * * @see #requestTotal() requestTotal() for more details. + * @since 4.8.0 * @return A pageable instance that will request the total size. */ - Pageable withTotal(); + default Pageable withTotal() { + if (this.requestTotal()) { + return this; + } + throw new UnsupportedOperationException("Changing requestTotal is not supported"); + } /** * Specify that the {@link Page} response should not have information about total size. + * * @see #requestTotal() requestTotal() for more details. + * @since 4.8.0 * @return A pageable instance that won't request the total size. */ - Pageable withoutTotal(); + default Pageable withoutTotal() { + if (!this.requestTotal()) { + return this; + } + throw new UnsupportedOperationException("Changing requestTotal is not supported"); + } /** * Creates a new {@link Pageable} at the given offset with a default size of 10. @@ -212,18 +260,52 @@ default List getOrderBy() { } /** - * Creates a new {@link Pageable} at the given offset. + * Creates a new {@link Pageable} with the given offset. + * * @param page The page * @param size the size * @param sort the sort * @return The pageable */ + static @NonNull Pageable from( + int page, + int size, + @Nullable Sort sort + ) { + return new DefaultPageable(page, size, sort, true); + } + + /** + * Creates a new {@link Pageable} with the given parameters. + * The method is used for deserialization and most likely should not be used as an API. + * + * @param page The page + * @param size The size + * @param mode The pagination mode + * @param cursor The currentCursor + * @param sort The sort + * @param requestTotal Whether to query total count + * @return The pageable + */ + @Internal @JsonCreator static @NonNull Pageable from( - @JsonProperty("number") int page, + @JsonProperty("page") int page, @JsonProperty("size") int size, - @JsonProperty("sort") @Nullable Sort sort) { - return new DefaultPageable(page, size, sort, true); + @JsonProperty("mode") @Nullable Mode mode, + @JsonProperty("cursor") @Nullable Cursor cursor, + @JsonProperty("nextCursor") @Nullable Cursor nextCursor, + @JsonProperty("sort") @Nullable Sort sort, + @JsonProperty(value = "requestTotal", defaultValue = "true") boolean requestTotal + ) { + if (mode == null || mode == Mode.OFFSET) { + return new DefaultPageable(page, size, sort, requestTotal); + } else { + return new DefaultCursoredPageable( + size, cursor, nextCursor, mode, page, + sort == null ? UNSORTED : sort, requestTotal + ); + } } /** @@ -247,10 +329,11 @@ default List getOrderBy() { } /** - * Create a new {@link Pageable} for forward pagination given the cursor after which to query. + * Create a new {@link Pageable} for forward pagination given the currentCursor after which to query. * - * @param cursor The cursor - * @param page The page number + * @since 4.8.0 + * @param cursor The currentCursor + * @param page The page page * @param size The page size * @param sort The sorting * @return The pageable @@ -259,14 +342,15 @@ default List getOrderBy() { if (sort == null) { sort = UNSORTED; } - return new DefaultCursoredPageable(size, cursor, null, false, page, sort, true); + return new DefaultCursoredPageable(size, cursor, null, Mode.CURSOR_NEXT, page, sort, true); } /** - * Create a new {@link Pageable} for backward pagination given the cursor after which to query. + * Create a new {@link Pageable} for backward pagination given the currentCursor after which to query. * - * @param cursor The cursor - * @param page The page number + * @since 4.8.0 + * @param cursor The currentCursor + * @param page The page page * @param size The page size * @param sort The sorting * @return The pageable @@ -275,24 +359,26 @@ default List getOrderBy() { if (sort == null) { sort = UNSORTED; } - return new DefaultCursoredPageable(size, null, cursor, true, page, sort, true); + return new DefaultCursoredPageable(size, cursor, null, Mode.CURSOR_PREVIOUS, page, sort, true); } /** - * The type of pagination: offset-based or cursor-based, which includes + * The type of pagination: offset-based or currentCursor-based, which includes * a direction. + * + * @since 4.8.0 */ enum Mode { /** - * Indicates forward cursor-based pagination, which follows the - * direction of the sort criteria, using a cursor that is + * Indicates forward currentCursor-based pagination, which follows the + * direction of the sort criteria, using a currentCursor that is * formed from the key of the last entity on the current page. */ CURSOR_NEXT, /** - * Indicates a request for a page with cursor-based pagination - * in the previous page direction to the sort criteria, using a cursor + * Indicates a request for a page with currentCursor-based pagination + * in the previous page direction to the sort criteria, using a currentCursor * that is formed from the key of first entity on the current page. * The order of results on each page follows the sort criteria * and is not reversed. @@ -302,8 +388,8 @@ enum Mode { /** * Indicates a request for a page using offset pagination. * The starting position for pages is computed as an offset from - * the first result based on the page number and maximum page size. - * Offset pagination is used when a cursor is not supplied. + * the first result based on the page page and maximum page size. + * Offset pagination is used when a currentCursor is not supplied. */ OFFSET } @@ -312,40 +398,44 @@ enum Mode { * An interface for defining pagination cursors. * It is generally a list of elements which can be used to create a query for the next * or previous page. + * + * @since 4.8.0 */ + @Serdeable interface Cursor { /** - * Returns the cursor element at the specified position. - * @param index The index of the cursor value - * @return The cursor value + * Returns the currentCursor element at the specified position. + * @param index The index of the currentCursor value + * @return The currentCursor value */ Object get(int index); /** - * Returns all the cursor values in a list. - * @return The cursor values + * Returns all the currentCursor values in a list. + * @return The currentCursor values */ List elements(); /** - * @return The number of elements in the cursor. + * @return The page of elements in the currentCursor. */ int size(); /** - * Create a cursor from elements. - * @param elements The cursor elements - * @return The cursor + * Create a currentCursor from elements. + * @param elements The currentCursor elements + * @return The currentCursor */ static Cursor of(Object... elements) { return new DefaultCursoredPageable.DefaultCursor(Arrays.asList(elements)); } /** - * Create a cursor from elements. - * @param elements The cursor elements - * @return The cursor + * Create a currentCursor from elements. + * @param elements The currentCursor elements + * @return The currentCursor */ + @JsonCreator static Cursor of(List elements) { return new DefaultCursoredPageable.DefaultCursor(elements); } diff --git a/data-model/src/main/java/io/micronaut/data/model/Slice.java b/data-model/src/main/java/io/micronaut/data/model/Slice.java index 155504e1444..d15d16ec7a4 100644 --- a/data-model/src/main/java/io/micronaut/data/model/Slice.java +++ b/data-model/src/main/java/io/micronaut/data/model/Slice.java @@ -57,7 +57,7 @@ public interface Slice extends Iterable { @NonNull Pageable getPageable(); /** - * @return The page number + * @return The page page */ default int getPageNumber() { return getPageable().getNumber(); @@ -107,7 +107,7 @@ default boolean isEmpty() { } /** - * @return The number of elements + * @return The page of elements */ default int getNumberOfElements() { return getContent().size(); diff --git a/data-model/src/main/java/io/micronaut/data/model/Sort.java b/data-model/src/main/java/io/micronaut/data/model/Sort.java index b209dacfa3c..f9087f0ab00 100644 --- a/data-model/src/main/java/io/micronaut/data/model/Sort.java +++ b/data-model/src/main/java/io/micronaut/data/model/Sort.java @@ -178,6 +178,19 @@ public String getProperty() { return property; } + /** + * Create an order that is reversed to current. + * + * @return A new instance of order that is reversed. + */ + public Order reverse() { + return new Order( + property, + direction == Direction.ASC ? Direction.DESC : Direction.ASC, + ignoreCase + ); + } + /** * Creates a new order for the given property in descending order. * diff --git a/data-model/src/main/java/io/micronaut/data/model/query/builder/sql/SqlQueryBuilder.java b/data-model/src/main/java/io/micronaut/data/model/query/builder/sql/SqlQueryBuilder.java index 84b769fb71c..372828ab161 100644 --- a/data-model/src/main/java/io/micronaut/data/model/query/builder/sql/SqlQueryBuilder.java +++ b/data-model/src/main/java/io/micronaut/data/model/query/builder/sql/SqlQueryBuilder.java @@ -44,6 +44,7 @@ import io.micronaut.data.model.Embedded; import io.micronaut.data.model.JsonDataType; import io.micronaut.data.model.Pageable; +import io.micronaut.data.model.Pageable.Mode; import io.micronaut.data.model.PersistentEntity; import io.micronaut.data.model.PersistentProperty; import io.micronaut.data.model.PersistentPropertyPath; @@ -1214,7 +1215,7 @@ public QueryResult buildPagination(@NonNull Pageable pageable) { int size = pageable.getSize(); if (size > 0) { StringBuilder builder = new StringBuilder(" "); - long from = pageable instanceof CursoredPageable ? 0 : pageable.getOffset(); + long from = pageable.getMode() == Mode.OFFSET ? pageable.getOffset() : 0; switch (dialect) { case H2: case MYSQL: diff --git a/data-model/src/test/groovy/io/micronaut/data/model/PageSpec.groovy b/data-model/src/test/groovy/io/micronaut/data/model/PageSpec.groovy index e7c2010c71c..fa710c2fa26 100644 --- a/data-model/src/test/groovy/io/micronaut/data/model/PageSpec.groovy +++ b/data-model/src/test/groovy/io/micronaut/data/model/PageSpec.groovy @@ -122,11 +122,40 @@ class PageSpec extends Specification { def json = serdeMapper.writeValueAsString(pageable) then: - json == '{"size":3,"number":0,"sort":{}}' + json == '{"size":3,"number":0,"mode":"OFFSET","sort":{}}' def deserializedPageable = serdeMapper.readValue(json, Pageable) deserializedPageable == pageable } + void "test serialization and deserialization of a cursored pageable - serde"() { + def pageable = Pageable.afterCursor( + Pageable.Cursor.of("value1", 2), + 0, 3, Sort.UNSORTED + ) + + when: + def json = serdeMapper.writeValueAsString(pageable) + + then: + json == '{"size":3,"cursor":{"elements":["value1",2]},"mode":"CURSOR_NEXT","number":0,"sort":{},"requestTotal":true}' + def deserializedPageable = serdeMapper.readValue(json, Pageable) + deserializedPageable == pageable + def deserializedPageable2 = serdeMapper.readValue(json, CursoredPageable) + deserializedPageable2 == pageable + } + + void "test sort serialization"() { + def sort = Sort.of(Sort.Order.asc("property")) + + when: + def json = serdeMapper.writeValueAsString(sort) + + then: + json == '{"orderBy":[{"ignoreCase":false,"direction":"ASC","property":"property","ascending":true}]}' + def deserializedSort = serdeMapper.readValue(json, Sort) + deserializedSort == sort + } + @EqualsAndHashCode @ToString @Serdeable diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/DefaultFindSliceInterceptor.java b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/DefaultFindSliceInterceptor.java index b387846f247..a046eaed9b8 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/DefaultFindSliceInterceptor.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/DefaultFindSliceInterceptor.java @@ -27,6 +27,9 @@ import io.micronaut.data.model.runtime.PagedQuery; import io.micronaut.data.model.runtime.PreparedQuery; import io.micronaut.data.operations.RepositoryOperations; +import io.micronaut.data.runtime.operations.internal.sql.DefaultSqlPreparedQuery; + +import java.util.List; /** * Default implementation of {@link FindSliceInterceptor}. @@ -53,7 +56,11 @@ public R intercept(RepositoryMethodKey methodKey, MethodInvocationContext PreparedQuery preparedQuery = prepareQuery(methodKey, context); Pageable pageable = preparedQuery.getPageable(); Iterable iterable = (Iterable) operations.findAll(preparedQuery); - Slice slice = Slice.of(CollectionUtils.iterableToList(iterable), pageable); + List results = CollectionUtils.iterableToList(iterable); + if (preparedQuery instanceof DefaultSqlPreparedQuery sqlPreparedQuery) { + pageable = sqlPreparedQuery.updatePageable(results, pageable); + } + Slice slice = Slice.of(results, pageable); return convertOrFail(context, slice); } else { PagedQuery pagedQuery = getPagedQuery(context); diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/DefaultSqlPreparedQuery.java b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/DefaultSqlPreparedQuery.java index cc828889ba0..e465e74d810 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/DefaultSqlPreparedQuery.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/DefaultSqlPreparedQuery.java @@ -174,13 +174,7 @@ public void attachPageable(Pageable pageable, boolean isSingleResult) { // Create a sort for the cursored pagination. The sort must produce a unique // sorting on the rows. Therefore, we make sure id is present in it. List orders = new ArrayList<>(sort.getOrderBy()); - List> idProperties; - if (persistentEntity.getIdentity() != null) { - idProperties = List.of(persistentEntity.getIdentity()); - } else { - idProperties = Arrays.stream(persistentEntity.getCompositeIdentity()).toList(); - } - for (PersistentProperty idProperty: idProperties) { + for (PersistentProperty idProperty: persistentEntity.getIdentityProperties()) { String name = idProperty.getName(); if (orders.stream().noneMatch(o -> o.getProperty().equals(name))) { orders.add(Order.asc(name)); @@ -190,9 +184,7 @@ public void attachPageable(Pageable pageable, boolean isSingleResult) { if (cursored.isBackward()) { sort = reverseSort(sort); } - added.append(buildCursorPagination( - cursored.isBackward() ? cursored.getEndCursor() : cursored.getStartCursor(), sort - )); + added.append(buildCursorPagination(cursored.cursor().orElse(null), sort)); } if (sort.isSorted()) { added.append(queryBuilder.buildOrderBy("", persistentEntity, sqlStoredQuery.getAnnotationMetadata(), sort, isNative()).getQuery()); @@ -227,15 +219,7 @@ private Sort reverseSort(Sort sort) { if (!sort.isSorted()) { return sort; } - List orders = new ArrayList<>(sort.getOrderBy().size()); - for (Order order : sort.getOrderBy()) { - orders.add(new Order( - order.getProperty(), - order.getDirection() == Direction.ASC ? Direction.DESC : Direction.ASC, - order.isIgnoreCase() - )); - } - return Sort.of(orders); + return Sort.of(sort.getOrderBy().stream().map(Order::reverse).toList()); } /** @@ -336,14 +320,14 @@ public Pageable updatePageable(List results, Pageable pageable) { } } else { if (cursored.isBackward()) { - endCursor = cursored.getEndCursor(); + endCursor = cursored.cursor().orElse(null); } else { - startCursor = cursored.getStartCursor(); + startCursor = cursored.cursor().orElse(null); } } return CursoredPageable.from( - cursored.getNumber(), startCursor, endCursor, cursored.isBackward(), cursored.getSize(), - cursored.getSort() + cursored.getNumber(), startCursor, endCursor, cursored.getMode(), cursored.getSize(), + cursored.getSort(), cursored.requestTotal() ); } return pageable; From 6bcabbad610349673e9e0a1f51eda8ce257805f9 Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Wed, 17 Apr 2024 15:28:03 -0400 Subject: [PATCH 12/20] Add tests --- ... => OracleXECursoredPaginationSpec.groovy} | 2 +- .../data/model/DefaultCursoredPageable.java | 4 +- .../java/io/micronaut/data/model/Page.java | 6 +-- .../r2dbc/h2/H2CursoredPaginationSpec.groovy | 48 +++++++++++++++++++ .../mysql/MySqlCursoredPaginationSpec.groovy | 47 ++++++++++++++++++ .../OracleXECursoredPaginationSpec.groovy | 47 ++++++++++++++++++ .../PostgresCursoredPaginationSpec.groovy | 47 ++++++++++++++++++ .../mapper/sql/SqlResultEntityTypeMapper.java | 1 + .../internal/sql/DefaultSqlPreparedQuery.java | 36 ++++++-------- 9 files changed, 210 insertions(+), 28 deletions(-) rename data-jdbc/src/test/groovy/io/micronaut/data/jdbc/oraclexe/{OracleXEPaginationSpec.groovy => OracleXECursoredPaginationSpec.groovy} (92%) create mode 100644 data-r2dbc/src/test/groovy/io/micronaut/data/r2dbc/h2/H2CursoredPaginationSpec.groovy create mode 100644 data-r2dbc/src/test/groovy/io/micronaut/data/r2dbc/mysql/MySqlCursoredPaginationSpec.groovy create mode 100644 data-r2dbc/src/test/groovy/io/micronaut/data/r2dbc/oraclexe/OracleXECursoredPaginationSpec.groovy create mode 100644 data-r2dbc/src/test/groovy/io/micronaut/data/r2dbc/postgres/PostgresCursoredPaginationSpec.groovy diff --git a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/oraclexe/OracleXEPaginationSpec.groovy b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/oraclexe/OracleXECursoredPaginationSpec.groovy similarity index 92% rename from data-jdbc/src/test/groovy/io/micronaut/data/jdbc/oraclexe/OracleXEPaginationSpec.groovy rename to data-jdbc/src/test/groovy/io/micronaut/data/jdbc/oraclexe/OracleXECursoredPaginationSpec.groovy index acf52db8bba..6bb64ef6a27 100644 --- a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/oraclexe/OracleXEPaginationSpec.groovy +++ b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/oraclexe/OracleXECursoredPaginationSpec.groovy @@ -23,7 +23,7 @@ import io.micronaut.data.tck.tests.AbstractCursoredPageSpec import spock.lang.AutoCleanup import spock.lang.Shared -class OracleXEPaginationSpec extends AbstractCursoredPageSpec implements OracleTestPropertyProvider { +class OracleXECursoredPaginationSpec extends AbstractCursoredPageSpec implements OracleTestPropertyProvider { @Shared @AutoCleanup ApplicationContext context diff --git a/data-model/src/main/java/io/micronaut/data/model/DefaultCursoredPageable.java b/data-model/src/main/java/io/micronaut/data/model/DefaultCursoredPageable.java index 0f4dd8bbabd..1d0ff2a5b41 100644 --- a/data-model/src/main/java/io/micronaut/data/model/DefaultCursoredPageable.java +++ b/data-model/src/main/java/io/micronaut/data/model/DefaultCursoredPageable.java @@ -117,12 +117,12 @@ public CursoredPageable next() { @Override public CursoredPageable previous() { - Cursor requiredCursor = mode == Mode.CURSOR_NEXT ? nextCursor : currentCursor; + Cursor requiredCursor = mode == Mode.CURSOR_PREVIOUS ? nextCursor : currentCursor; if (requiredCursor != null) { return new DefaultCursoredPageable( size, - null, requiredCursor, + null, Mode.CURSOR_PREVIOUS, Math.max(page - 1, 0), sort, diff --git a/data-model/src/main/java/io/micronaut/data/model/Page.java b/data-model/src/main/java/io/micronaut/data/model/Page.java index 373434e76d3..3f9d1d64e67 100644 --- a/data-model/src/main/java/io/micronaut/data/model/Page.java +++ b/data-model/src/main/java/io/micronaut/data/model/Page.java @@ -101,10 +101,10 @@ default boolean hasNext() { * @return Whether there exist a previous page. */ default boolean hasPrevious() { - if (getPageable() instanceof CursoredPageable cursoredPageable) { - return cursoredPageable.hasPrevious(); + if (getPageable().getMode() == Mode.OFFSET) { + return getOffset() > 0; } - return getOffset() > 0; + return getPageable().hasPrevious(); } /** diff --git a/data-r2dbc/src/test/groovy/io/micronaut/data/r2dbc/h2/H2CursoredPaginationSpec.groovy b/data-r2dbc/src/test/groovy/io/micronaut/data/r2dbc/h2/H2CursoredPaginationSpec.groovy new file mode 100644 index 00000000000..3bae50d78b5 --- /dev/null +++ b/data-r2dbc/src/test/groovy/io/micronaut/data/r2dbc/h2/H2CursoredPaginationSpec.groovy @@ -0,0 +1,48 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.data.r2dbc.h2 + +import groovy.transform.Memoized +import io.micronaut.context.ApplicationContext +import io.micronaut.data.tck.repositories.* +import io.micronaut.data.tck.tests.AbstractCursoredPageSpec +import io.micronaut.data.tck.tests.AbstractRepositorySpec +import spock.lang.AutoCleanup +import spock.lang.Shared + +class H2CursoredPaginationSpec extends AbstractCursoredPageSpec implements H2TestPropertyProvider { + + @Shared + @AutoCleanup + ApplicationContext context + + @Memoized + @Override + PersonRepository getPersonRepository() { + return context.getBean(H2PersonRepository) + } + + @Memoized + @Override + BookRepository getBookRepository() { + return context.getBean(H2BookRepository) + } + + @Override + void init() { + context = ApplicationContext.run(properties) + } +} diff --git a/data-r2dbc/src/test/groovy/io/micronaut/data/r2dbc/mysql/MySqlCursoredPaginationSpec.groovy b/data-r2dbc/src/test/groovy/io/micronaut/data/r2dbc/mysql/MySqlCursoredPaginationSpec.groovy new file mode 100644 index 00000000000..69849a7ce1d --- /dev/null +++ b/data-r2dbc/src/test/groovy/io/micronaut/data/r2dbc/mysql/MySqlCursoredPaginationSpec.groovy @@ -0,0 +1,47 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.data.r2dbc.mysql + +import groovy.transform.Memoized +import io.micronaut.context.ApplicationContext +import io.micronaut.data.tck.repositories.BookRepository +import io.micronaut.data.tck.repositories.PersonRepository +import io.micronaut.data.tck.tests.AbstractCursoredPageSpec +import spock.lang.AutoCleanup +import spock.lang.Shared + +class MySqlCursoredPaginationSpec extends AbstractCursoredPageSpec implements MySqlTestPropertyProvider { + + @Shared @AutoCleanup ApplicationContext context + + @Memoized + @Override + PersonRepository getPersonRepository() { + return context.getBean(MySqlPersonRepository) + } + + @Memoized + @Override + BookRepository getBookRepository() { + return context.getBean(MySqlBookRepository) + } + + @Override + void init() { + context = ApplicationContext.run(properties) + } + +} diff --git a/data-r2dbc/src/test/groovy/io/micronaut/data/r2dbc/oraclexe/OracleXECursoredPaginationSpec.groovy b/data-r2dbc/src/test/groovy/io/micronaut/data/r2dbc/oraclexe/OracleXECursoredPaginationSpec.groovy new file mode 100644 index 00000000000..d739f88256b --- /dev/null +++ b/data-r2dbc/src/test/groovy/io/micronaut/data/r2dbc/oraclexe/OracleXECursoredPaginationSpec.groovy @@ -0,0 +1,47 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.data.r2dbc.oraclexe + +import groovy.transform.Memoized +import io.micronaut.context.ApplicationContext +import io.micronaut.data.tck.repositories.BookRepository +import io.micronaut.data.tck.repositories.PersonRepository +import io.micronaut.data.tck.tests.AbstractCursoredPageSpec +import spock.lang.AutoCleanup +import spock.lang.Shared + +class OracleXECursoredPaginationSpec extends AbstractCursoredPageSpec implements OracleXETestPropertyProvider { + + @Shared @AutoCleanup ApplicationContext context + + @Override + @Memoized + PersonRepository getPersonRepository() { + return context.getBean(OracleXEPersonRepository) + } + + @Override + @Memoized + BookRepository getBookRepository() { + return context.getBean(OracleXEBookRepository) + } + + @Override + void init() { + context = ApplicationContext.run(properties) + } + +} diff --git a/data-r2dbc/src/test/groovy/io/micronaut/data/r2dbc/postgres/PostgresCursoredPaginationSpec.groovy b/data-r2dbc/src/test/groovy/io/micronaut/data/r2dbc/postgres/PostgresCursoredPaginationSpec.groovy new file mode 100644 index 00000000000..1efee4a74ad --- /dev/null +++ b/data-r2dbc/src/test/groovy/io/micronaut/data/r2dbc/postgres/PostgresCursoredPaginationSpec.groovy @@ -0,0 +1,47 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.data.r2dbc.postgres + +import groovy.transform.Memoized +import io.micronaut.context.ApplicationContext +import io.micronaut.data.tck.repositories.BookRepository +import io.micronaut.data.tck.repositories.PersonRepository +import io.micronaut.data.tck.tests.AbstractCursoredPageSpec +import spock.lang.AutoCleanup +import spock.lang.Ignore +import spock.lang.Shared + +@Ignore("Causes error: 'FATAL: sorry, too many clients already'") +class PostgresCursoredPaginationSpec extends AbstractCursoredPageSpec implements PostgresTestPropertyProvider { + @Shared @AutoCleanup ApplicationContext context + + @Memoized + @Override + PersonRepository getPersonRepository() { + return context.getBean(PostgresPersonRepository) + } + + @Memoized + @Override + BookRepository getBookRepository() { + return context.getBean(PostgresBookRepository) + } + + @Override + void init() { + context = ApplicationContext.run(getProperties()) + } +} diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/mapper/sql/SqlResultEntityTypeMapper.java b/data-runtime/src/main/java/io/micronaut/data/runtime/mapper/sql/SqlResultEntityTypeMapper.java index 19625ef9601..2d0f51fe3e0 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/mapper/sql/SqlResultEntityTypeMapper.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/mapper/sql/SqlResultEntityTypeMapper.java @@ -46,6 +46,7 @@ import java.sql.SQLException; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.ListIterator; diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/DefaultSqlPreparedQuery.java b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/DefaultSqlPreparedQuery.java index e465e74d810..9cf2c3f4b58 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/DefaultSqlPreparedQuery.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/DefaultSqlPreparedQuery.java @@ -297,36 +297,28 @@ private String buildCursorPagination(@Nullable Pageable.Cursor cursor, @NonNull */ public Pageable updatePageable(List results, Pageable pageable) { if (pageable instanceof CursoredPageable cursored) { - if (cursored.isBackward()) { - Collections.reverse(results); - } - - Cursor startCursor = null; - Cursor endCursor = null; + Cursor cursor = cursored.cursor().orElse(null); + Cursor nextCursor = null; if (!results.isEmpty()) { - if (!cursored.isBackward() || results.size() == cursored.getSize()) { - E firstValue = (E) results.get(0); - startCursor = Cursor.of(new ArrayList<>(cursorProperties.size())); - for (RuntimePersistentProperty property : cursorProperties) { - startCursor.elements().add(property.getProperty().get(firstValue)); - } + E firstValue = (E) results.get(0); + cursor = Cursor.of(new ArrayList<>(cursorProperties.size())); + for (RuntimePersistentProperty property : cursorProperties) { + cursor.elements().add(property.getProperty().get(firstValue)); } - if (cursored.isBackward() || results.size() == cursored.getSize()) { + if (results.size() == cursored.getSize()) { E lastValue = (E) results.get(results.size() - 1); - endCursor = Cursor.of(new ArrayList<>(cursorProperties.size())); + nextCursor = Cursor.of(new ArrayList<>(cursorProperties.size())); for (RuntimePersistentProperty property : cursorProperties) { - endCursor.elements().add(property.getProperty().get(lastValue)); + nextCursor.elements().add(property.getProperty().get(lastValue)); } } - } else { - if (cursored.isBackward()) { - endCursor = cursored.cursor().orElse(null); - } else { - startCursor = cursored.cursor().orElse(null); - } + } + + if (cursored.isBackward()) { + Collections.reverse(results); } return CursoredPageable.from( - cursored.getNumber(), startCursor, endCursor, cursored.getMode(), cursored.getSize(), + cursored.getNumber(), cursor, nextCursor, cursored.getMode(), cursored.getSize(), cursored.getSort(), cursored.requestTotal() ); } From fbc85702e918a601a764b7e7dc379918d8bb5638 Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Wed, 17 Apr 2024 17:23:24 -0400 Subject: [PATCH 13/20] Fix checkstyle --- data-model/src/main/java/io/micronaut/data/model/Pageable.java | 3 ++- .../data/model/query/builder/sql/SqlQueryBuilder.java | 1 - .../src/test/groovy/io/micronaut/data/model/PageSpec.groovy | 2 +- .../operations/internal/sql/DefaultSqlPreparedQuery.java | 2 -- 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/data-model/src/main/java/io/micronaut/data/model/Pageable.java b/data-model/src/main/java/io/micronaut/data/model/Pageable.java index 34f7ba5bac4..8a95f0d1294 100644 --- a/data-model/src/main/java/io/micronaut/data/model/Pageable.java +++ b/data-model/src/main/java/io/micronaut/data/model/Pageable.java @@ -282,7 +282,8 @@ default Pageable withoutTotal() { * @param page The page * @param size The size * @param mode The pagination mode - * @param cursor The currentCursor + * @param cursor The current cursor + * @param nextCursor The next cursor * @param sort The sort * @param requestTotal Whether to query total count * @return The pageable diff --git a/data-model/src/main/java/io/micronaut/data/model/query/builder/sql/SqlQueryBuilder.java b/data-model/src/main/java/io/micronaut/data/model/query/builder/sql/SqlQueryBuilder.java index 372828ab161..902a7067a45 100644 --- a/data-model/src/main/java/io/micronaut/data/model/query/builder/sql/SqlQueryBuilder.java +++ b/data-model/src/main/java/io/micronaut/data/model/query/builder/sql/SqlQueryBuilder.java @@ -39,7 +39,6 @@ import io.micronaut.data.annotation.sql.SqlMembers; import io.micronaut.data.exceptions.MappingException; import io.micronaut.data.model.Association; -import io.micronaut.data.model.CursoredPageable; import io.micronaut.data.model.DataType; import io.micronaut.data.model.Embedded; import io.micronaut.data.model.JsonDataType; diff --git a/data-model/src/test/groovy/io/micronaut/data/model/PageSpec.groovy b/data-model/src/test/groovy/io/micronaut/data/model/PageSpec.groovy index fa710c2fa26..0084707d51c 100644 --- a/data-model/src/test/groovy/io/micronaut/data/model/PageSpec.groovy +++ b/data-model/src/test/groovy/io/micronaut/data/model/PageSpec.groovy @@ -122,7 +122,7 @@ class PageSpec extends Specification { def json = serdeMapper.writeValueAsString(pageable) then: - json == '{"size":3,"number":0,"mode":"OFFSET","sort":{}}' + json == '{"size":3,"number":0,"sort":{},"mode":"OFFSET"}' def deserializedPageable = serdeMapper.readValue(json, Pageable) deserializedPageable == pageable } diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/DefaultSqlPreparedQuery.java b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/DefaultSqlPreparedQuery.java index 9cf2c3f4b58..406d8a66319 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/DefaultSqlPreparedQuery.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/DefaultSqlPreparedQuery.java @@ -26,7 +26,6 @@ import io.micronaut.data.model.PersistentProperty; import io.micronaut.data.model.Sort; import io.micronaut.data.model.Sort.Order; -import io.micronaut.data.model.Sort.Order.Direction; import io.micronaut.data.model.query.builder.AbstractSqlLikeQueryBuilder; import io.micronaut.data.model.query.builder.sql.Dialect; import io.micronaut.data.model.query.builder.sql.SqlQueryBuilder; @@ -42,7 +41,6 @@ import java.lang.reflect.Array; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; From 019dacd68cefa6c0294939767515ecd6362e2fb7 Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Wed, 17 Apr 2024 20:18:55 -0400 Subject: [PATCH 14/20] Throw UnsupportedOperationException where cursored pageable is not yet supported --- .../model/query/builder/CosmosSqlQueryBuilder.java | 4 ++++ .../hibernate/operations/AbstractHibernateOperations.java | 7 +++++++ .../data/mongodb/operations/DefaultMongoPreparedQuery.java | 7 +++++++ .../operations/DefaultMongoRepositoryOperations.java | 7 +++++++ .../criteria/AbstractSpecificationInterceptor.java | 4 ++++ .../reactive/AbstractReactiveSpecificationInterceptor.java | 4 ++++ 6 files changed, 33 insertions(+) diff --git a/data-document-model/src/main/java/io/micronaut/data/document/model/query/builder/CosmosSqlQueryBuilder.java b/data-document-model/src/main/java/io/micronaut/data/document/model/query/builder/CosmosSqlQueryBuilder.java index af3a45d39a6..f5dc067e7e8 100644 --- a/data-document-model/src/main/java/io/micronaut/data/document/model/query/builder/CosmosSqlQueryBuilder.java +++ b/data-document-model/src/main/java/io/micronaut/data/document/model/query/builder/CosmosSqlQueryBuilder.java @@ -26,6 +26,7 @@ import io.micronaut.data.model.Association; import io.micronaut.data.model.Embedded; import io.micronaut.data.model.Pageable; +import io.micronaut.data.model.Pageable.Mode; import io.micronaut.data.model.PersistentEntity; import io.micronaut.data.model.PersistentProperty; import io.micronaut.data.model.PersistentPropertyPath; @@ -329,6 +330,9 @@ public Map getAdditionalRequiredParameters() { @NonNull @Override public QueryResult buildPagination(@NonNull Pageable pageable) { + if (pageable.getMode() != Mode.OFFSET) { + throw new UnsupportedOperationException("Pageable mode " + pageable.getMode() + " is not supported by cosmos operations"); + } int size = pageable.getSize(); if (size > 0) { StringBuilder builder = new StringBuilder(" "); diff --git a/data-hibernate-jpa/src/main/java/io/micronaut/data/hibernate/operations/AbstractHibernateOperations.java b/data-hibernate-jpa/src/main/java/io/micronaut/data/hibernate/operations/AbstractHibernateOperations.java index 95adbbd243c..b9bcbc21877 100644 --- a/data-hibernate-jpa/src/main/java/io/micronaut/data/hibernate/operations/AbstractHibernateOperations.java +++ b/data-hibernate-jpa/src/main/java/io/micronaut/data/hibernate/operations/AbstractHibernateOperations.java @@ -29,6 +29,7 @@ import io.micronaut.data.annotation.QueryHint; import io.micronaut.data.jpa.annotation.EntityGraph; import io.micronaut.data.model.Pageable; +import io.micronaut.data.model.Pageable.Mode; import io.micronaut.data.model.Sort; import io.micronaut.data.model.query.builder.jpa.JpaQueryBuilder; import io.micronaut.data.model.runtime.PagedQuery; @@ -335,6 +336,9 @@ protected void collectFindAll(S session, PreparedQuery preparedQuery, String queryStr = preparedQuery.getQuery(); Pageable pageable = preparedQuery.getPageable(); if (pageable != Pageable.UNPAGED) { + if (pageable.getMode() != Mode.OFFSET) { + throw new UnsupportedOperationException("Pageable mode " + pageable.getMode() + " is not supported by hibernate operations"); + } Sort sort = pageable.getSort(); if (sort.isSorted()) { queryStr += QUERY_BUILDER.buildOrderBy(queryStr, getEntity(preparedQuery.getRootEntity()), AnnotationMetadata.EMPTY_METADATA, sort, @@ -599,6 +603,9 @@ private void bindPageable(P q, @NonNull Pageable pageable) { // no pagination return; } + if (pageable.getMode() != Mode.OFFSET) { + throw new UnsupportedOperationException("Pageable mode " + pageable.getMode() + " is not supported by hibernate operations"); + } int max = pageable.getSize(); if (max > 0) { diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/operations/DefaultMongoPreparedQuery.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/operations/DefaultMongoPreparedQuery.java index d699ad986ab..a434d25bc67 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/operations/DefaultMongoPreparedQuery.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/operations/DefaultMongoPreparedQuery.java @@ -18,6 +18,7 @@ import com.mongodb.client.model.Sorts; import io.micronaut.core.annotation.Internal; import io.micronaut.data.model.Pageable; +import io.micronaut.data.model.Pageable.Mode; import io.micronaut.data.model.Sort; import io.micronaut.data.model.runtime.PreparedQuery; import io.micronaut.data.model.runtime.RuntimePersistentEntity; @@ -81,6 +82,9 @@ public MongoFind getFind() { MongoFind find = mongoStoredQuery.getFind(defaultPreparedQuery.getContext()); Pageable pageable = defaultPreparedQuery.getPageable(); if (pageable != Pageable.UNPAGED) { + if (pageable.getMode() != Mode.OFFSET) { + throw new UnsupportedOperationException("Mode " + pageable.getMode() + " is not supported by the MongoDB implementation"); + } MongoFindOptions findOptions = find.getOptions(); MongoFindOptions options = findOptions == null ? new MongoFindOptions() : new MongoFindOptions(findOptions); options.limit(pageable.getSize()).skip((int) pageable.getOffset()); @@ -113,6 +117,9 @@ public PreparedQuery getPreparedQueryDelegate() { private int applyPageable(Pageable pageable, List pipeline) { int limit = 0; if (pageable != Pageable.UNPAGED) { + if (pageable.getMode() != Mode.OFFSET) { + throw new UnsupportedOperationException("Mode " + pageable.getMode() + " is not supported by the MongoDB implementation"); + } int skip = (int) pageable.getOffset(); limit = pageable.getSize(); Sort pageableSort = pageable.getSort(); diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/operations/DefaultMongoRepositoryOperations.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/operations/DefaultMongoRepositoryOperations.java index c0b453c9bbf..c09ca93a54e 100644 --- a/data-mongodb/src/main/java/io/micronaut/data/mongodb/operations/DefaultMongoRepositoryOperations.java +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/operations/DefaultMongoRepositoryOperations.java @@ -46,6 +46,7 @@ import io.micronaut.data.exceptions.DataAccessException; import io.micronaut.data.model.Page; import io.micronaut.data.model.Pageable; +import io.micronaut.data.model.Pageable.Mode; import io.micronaut.data.model.PersistentEntity; import io.micronaut.data.model.PersistentProperty; import io.micronaut.data.model.runtime.AttributeConverterRegistry; @@ -316,6 +317,9 @@ private Iterable findAllAggregated(ClientSession clientSession, MongoPreparedQuery preparedQuery, boolean stream) { Pageable pageable = preparedQuery.getPageable(); + if (pageable.getMode() != Mode.OFFSET) { + throw new UnsupportedOperationException("Mode " + pageable.getMode() + " is not supported by the MongoDB implementation"); + } int limit = pageable == Pageable.UNPAGED ? -1 : pageable.getSize(); Class type = preparedQuery.getRootEntity(); Class resultType = preparedQuery.getResultType(); @@ -334,6 +338,9 @@ private Iterable findAllFiltered(ClientSession clientSession, MongoPreparedQuery preparedQuery, boolean stream) { Pageable pageable = preparedQuery.getPageable(); + if (pageable.getMode() != Mode.OFFSET) { + throw new UnsupportedOperationException("Mode " + pageable.getMode() + " is not supported by the MongoDB implementation"); + } int limit = pageable == Pageable.UNPAGED ? -1 : pageable.getSize(); Class type = preparedQuery.getRootEntity(); Class resultType = preparedQuery.getResultType(); diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/criteria/AbstractSpecificationInterceptor.java b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/criteria/AbstractSpecificationInterceptor.java index 447b0726ffd..369fffc501a 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/criteria/AbstractSpecificationInterceptor.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/criteria/AbstractSpecificationInterceptor.java @@ -27,6 +27,7 @@ import io.micronaut.data.intercept.RepositoryMethodKey; import io.micronaut.data.model.AssociationUtils; import io.micronaut.data.model.Pageable; +import io.micronaut.data.model.Pageable.Mode; import io.micronaut.data.model.Sort; import io.micronaut.data.model.jpa.criteria.PersistentEntityFrom; import io.micronaut.data.model.jpa.criteria.impl.QueryResultPersistentEntityCriteriaQuery; @@ -141,6 +142,9 @@ protected final Iterable findAll(RepositoryMethodKey methodKey, MethodInvocat CriteriaQuery query = buildQuery(context, type, methodJoinPaths); Pageable pageable = getPageable(context); if (pageable != null) { + if (pageable.getMode() != Mode.OFFSET) { + throw new UnsupportedOperationException("Pageable mode " + pageable.getMode() + " is not supported with specifications"); + } return criteriaRepositoryOperations.findAll(query, (int) pageable.getOffset(), pageable.getSize()); } return criteriaRepositoryOperations.findAll(query); diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/criteria/reactive/AbstractReactiveSpecificationInterceptor.java b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/criteria/reactive/AbstractReactiveSpecificationInterceptor.java index 0e50b10d84e..358de4a2e0b 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/criteria/reactive/AbstractReactiveSpecificationInterceptor.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/criteria/reactive/AbstractReactiveSpecificationInterceptor.java @@ -20,6 +20,7 @@ import io.micronaut.data.exceptions.DataAccessException; import io.micronaut.data.intercept.RepositoryMethodKey; import io.micronaut.data.model.Pageable; +import io.micronaut.data.model.Pageable.Mode; import io.micronaut.data.model.query.JoinPath; import io.micronaut.data.operations.RepositoryOperations; import io.micronaut.data.operations.reactive.ReactiveCapableRepository; @@ -76,6 +77,9 @@ protected final Publisher findAllReactive(RepositoryMethodKey methodKey, CriteriaQuery criteriaQuery = buildQuery(context, type, methodJoinPaths); Pageable pageable = getPageable(context); if (pageable != null) { + if (pageable.getMode() != Mode.OFFSET) { + throw new UnsupportedOperationException("Pageable mode " + pageable.getMode() + " is not supported by hibernate operations"); + } return reactiveCriteriaOperations.findAll(criteriaQuery, (int) pageable.getOffset(), pageable.getSize()); } return reactiveCriteriaOperations.findAll(criteriaQuery); From db7cc087871b40d190313efca1573ae920ece8e2 Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Thu, 18 Apr 2024 19:14:19 -0400 Subject: [PATCH 15/20] Add all cursors to the page implementation and remove nextPageable and previousPageable implementation from the CursoredPageable --- .../data/model/CursoredPageable.java | 22 +-- .../data/model/DefaultCursoredPage.java | 132 ++++++++++++++++++ .../data/model/DefaultCursoredPageable.java | 52 +------ .../java/io/micronaut/data/model/Page.java | 70 ++++++---- .../io/micronaut/data/model/Pageable.java | 31 +--- .../java/io/micronaut/data/model/Slice.java | 34 ++++- .../io/micronaut/data/model/PageSpec.groovy | 7 + .../intercept/DefaultFindPageInterceptor.java | 16 ++- .../DefaultFindSliceInterceptor.java | 4 - .../internal/sql/DefaultSqlPreparedQuery.java | 41 ++---- 10 files changed, 256 insertions(+), 153 deletions(-) create mode 100644 data-model/src/main/java/io/micronaut/data/model/DefaultCursoredPage.java diff --git a/data-model/src/main/java/io/micronaut/data/model/CursoredPageable.java b/data-model/src/main/java/io/micronaut/data/model/CursoredPageable.java index 153d481a0f6..fe98ef8a6ce 100644 --- a/data-model/src/main/java/io/micronaut/data/model/CursoredPageable.java +++ b/data-model/src/main/java/io/micronaut/data/model/CursoredPageable.java @@ -39,7 +39,7 @@ public interface CursoredPageable extends Pageable { * Constant for no pagination. */ CursoredPageable UNPAGED = new DefaultCursoredPageable( - -1, null, null, Mode.CURSOR_NEXT, 0, Sort.UNSORTED, true + -1, null, Mode.CURSOR_NEXT, 0, Sort.UNSORTED, true ); /** @@ -54,16 +54,6 @@ default Mode getMode() { return isBackward() ? Mode.CURSOR_PREVIOUS : Mode.CURSOR_NEXT; } - @Override - default @NonNull CursoredPageable next() { - throw new IllegalStateException("Cannot retrieve next page, as a currentCursor for that is not present"); - } - - @Override - default @NonNull CursoredPageable previous() { - throw new IllegalStateException("Cannot retrieve previous page, as a currentCursor for that is not present"); - } - /** * Creates a new {@link CursoredPageable} with the given sort. * @@ -75,7 +65,7 @@ default Mode getMode() { return UNPAGED; } return new DefaultCursoredPageable( - -1, null, null, Mode.CURSOR_NEXT, 0, sort, true + -1, null, Mode.CURSOR_NEXT, 0, sort, true ); } @@ -93,7 +83,7 @@ default Mode getMode() { if (sort == null) { sort = UNSORTED; } - return new DefaultCursoredPageable(size, null, null, Mode.CURSOR_NEXT, 0, sort, true); + return new DefaultCursoredPageable(size, null, Mode.CURSOR_NEXT, 0, sort, true); } /** @@ -101,7 +91,6 @@ default Mode getMode() { * * @param page The page * @param cursor The current currentCursor that will be used for querying data. - * @param nextCursor The currentCursor that could be used for querying the next page of data. * @param mode The pagination mode. Must be either forward or backward currentCursor pagination. * @param size The page size * @param sort The sort @@ -111,9 +100,8 @@ default Mode getMode() { @Internal @JsonCreator static @NonNull CursoredPageable from( - @JsonProperty("page") int page, + @JsonProperty("number") int page, @Nullable Cursor cursor, - @Nullable Cursor nextCursor, Pageable.Mode mode, int size, @Nullable Sort sort, @@ -122,7 +110,7 @@ default Mode getMode() { if (sort == null) { sort = UNSORTED; } - return new DefaultCursoredPageable(size, cursor, nextCursor, mode, page, sort, requestTotal); + return new DefaultCursoredPageable(size, cursor, mode, page, sort, requestTotal); } /** diff --git a/data-model/src/main/java/io/micronaut/data/model/DefaultCursoredPage.java b/data-model/src/main/java/io/micronaut/data/model/DefaultCursoredPage.java new file mode 100644 index 00000000000..10d9917f6c7 --- /dev/null +++ b/data-model/src/main/java/io/micronaut/data/model/DefaultCursoredPage.java @@ -0,0 +1,132 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.data.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.micronaut.core.annotation.Creator; +import io.micronaut.core.annotation.ReflectiveAccess; +import io.micronaut.data.model.Pageable.Cursor; +import io.micronaut.data.model.Pageable.Mode; +import io.micronaut.serde.annotation.Serdeable; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +/** + * Implementation of {@link Page} to return when {@link CursoredPageable} is requested. + * + * @author Andriy Dmytruk + * @since 4.8.0 + * @param The generic type + */ +@Serdeable +class DefaultCursoredPage extends DefaultPage { + + private final List cursors; + + /** + * Default constructor. + * @param content The content + * @param pageable The pageable + * @param totalSize The total size + */ + @JsonCreator + @Creator + @ReflectiveAccess + DefaultCursoredPage( + @JsonProperty("content") + List content, + @JsonProperty("pageable") + Pageable pageable, + @JsonProperty("cursors") + List cursors, + @JsonProperty("totalSize") + Long totalSize + ) { + super(content, pageable, totalSize); + if (content.size() != cursors.size()) { + throw new IllegalArgumentException("The number of cursors must match the number of content items for a page"); + } + this.cursors = cursors; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof DefaultCursoredPage that)) { + return false; + } + return Objects.equals(cursors, that.cursors) && super.equals(o); + } + + @Override + public Optional getCursor(int i) { + return Optional.of(cursors.get(i)); + } + + @Override + public boolean hasNext() { + Pageable pageable = getPageable(); + if (pageable.getMode() == Mode.CURSOR_NEXT) { + return cursors.size() == pageable.getSize(); + } else { + return true; + } + } + + @Override + public boolean hasPrevious() { + Pageable pageable = getPageable(); + if (pageable.getMode() == Mode.CURSOR_PREVIOUS) { + return cursors.size() == pageable.getSize(); + } else { + return true; + } + } + + @Override + public Pageable nextPageable() { + Pageable pageable = getPageable(); + Cursor cursor = cursors.isEmpty() ? pageable.cursor().orElse(null) : cursors.get(cursors.size() - 1); + return Pageable.afterCursor(cursor, pageable.getNumber() + 1, pageable.getSize(), pageable.getSort()); + } + + @Override + public Pageable previousPageable() { + Pageable pageable = getPageable(); + Cursor cursor = cursors.isEmpty() ? pageable.cursor().orElse(null) : cursors.get(0); + return Pageable.beforeCursor(cursor, Math.max(0, pageable.getNumber() - 1), pageable.getSize(), pageable.getSort()); + } + + @Override + public int hashCode() { + return Objects.hash(cursors, super.hashCode()); + } + + @Override + public String toString() { + return "DefaultPage{" + + "totalSize=" + getTotalSize() + + ",content=" + getContent() + + ",pageable=" + getPageable() + + ",cursors=" + cursors + + '}'; + } +} diff --git a/data-model/src/main/java/io/micronaut/data/model/DefaultCursoredPageable.java b/data-model/src/main/java/io/micronaut/data/model/DefaultCursoredPageable.java index 1d0ff2a5b41..c7f30f8bcde 100644 --- a/data-model/src/main/java/io/micronaut/data/model/DefaultCursoredPageable.java +++ b/data-model/src/main/java/io/micronaut/data/model/DefaultCursoredPageable.java @@ -31,9 +31,6 @@ * @param page The page. * @param currentCursor The current currentCursor. This is the currentCursor that will be used for pagination * in case this pageable is used in a query. - * @param nextCursor A currentCursor for the next page of data. It is stored in pageable to correctly - * support {@link #next()} for forward or {@link #previous()} for backward - * pagination. * @param mode The pagination mode. Could be one of {@link Mode#CURSOR_NEXT} or {@link Mode#CURSOR_PREVIOUS}. * @param size The size of a page * @param sort The sorting @@ -48,8 +45,6 @@ record DefaultCursoredPageable( @Nullable @JsonProperty("cursor") Cursor currentCursor, - @Nullable - Cursor nextCursor, Mode mode, @JsonProperty("number") int page, Sort sort, @@ -100,36 +95,13 @@ public boolean isBackward() { @Override public CursoredPageable next() { - Cursor requiredCursor = mode == Mode.CURSOR_PREVIOUS ? currentCursor : nextCursor; - if (requiredCursor != null) { - return new DefaultCursoredPageable( - size, - requiredCursor, - null, - Mode.CURSOR_NEXT, - page + 1, - sort, - requestTotal - ); - } - return CursoredPageable.super.next(); + throw new UnsupportedOperationException("To get next pageable results must be retrieved. Use page.nextPageable() to retrieve the next pageable."); } @Override public CursoredPageable previous() { - Cursor requiredCursor = mode == Mode.CURSOR_PREVIOUS ? nextCursor : currentCursor; - if (requiredCursor != null) { - return new DefaultCursoredPageable( - size, - requiredCursor, - null, - Mode.CURSOR_PREVIOUS, - Math.max(page - 1, 0), - sort, - requestTotal - ); - } - return CursoredPageable.super.previous(); + throw new UnsupportedOperationException("To get next pageable results must be retrieved. Use page.nextPageable() to retrieve the next pageable."); + } @Override @@ -137,7 +109,7 @@ public Pageable withTotal() { if (requestTotal) { return this; } - return new DefaultCursoredPageable(size, currentCursor, nextCursor, mode, page, sort, true); + return new DefaultCursoredPageable(size, currentCursor, mode, page, sort, true); } @Override @@ -145,17 +117,7 @@ public Pageable withoutTotal() { if (!requestTotal) { return this; } - return new DefaultCursoredPageable(size, currentCursor, nextCursor, mode, page, sort, true); - } - - @Override - public boolean hasNext() { - return mode == Mode.CURSOR_PREVIOUS ? currentCursor != null : nextCursor != null; - } - - @Override - public boolean hasPrevious() { - return mode == Mode.CURSOR_PREVIOUS ? nextCursor != null : currentCursor != null; + return new DefaultCursoredPageable(size, currentCursor, mode, page, sort, true); } @Override @@ -168,14 +130,13 @@ public boolean equals(Object o) { } return size == that.size && Objects.equals(currentCursor, that.currentCursor) - && Objects.equals(nextCursor, that.nextCursor) && Objects.equals(mode, that.mode) && Objects.equals(sort, that.sort); } @Override public int hashCode() { - return Objects.hash(size, currentCursor, nextCursor, mode, sort); + return Objects.hash(size, currentCursor, mode, sort); } @Override @@ -184,7 +145,6 @@ public String toString() { "size=" + size + ", page=" + page + ", currentCursor=" + currentCursor + - ", nextCursor=" + nextCursor + ", mode=" + mode + ", sort=" + sort + '}'; diff --git a/data-model/src/main/java/io/micronaut/data/model/Page.java b/data-model/src/main/java/io/micronaut/data/model/Page.java index 3f9d1d64e67..107d92429a0 100644 --- a/data-model/src/main/java/io/micronaut/data/model/Page.java +++ b/data-model/src/main/java/io/micronaut/data/model/Page.java @@ -24,11 +24,12 @@ import io.micronaut.core.annotation.Nullable; import io.micronaut.core.annotation.ReflectiveAccess; import io.micronaut.core.annotation.TypeHint; -import io.micronaut.data.model.Pageable.Mode; +import io.micronaut.data.model.Pageable.Cursor; import io.micronaut.serde.annotation.Serdeable; import java.util.Collections; import java.util.List; +import java.util.Optional; import java.util.function.Function; import java.util.stream.Collectors; @@ -80,31 +81,25 @@ default int getTotalPages() { } /** - * Determines whether there is a next page. + * Get cursor at the given position. + * Cursors are only available if {@code getPageable().getMode()} is one of + * {@link Pageable.Mode#CURSOR_NEXT} or {@link Pageable.Mode#CURSOR_PREVIOUS}. + * In that case there is a cursor for each of the data entities in the same order. + * To start pagination after or before a cursor create a pageable from it using the + * same sorting as before. * - * @since 4.8.0 - * @return Whether there exist a next page. + * @param i The index of cursor to retrieve. + * @return The cursor at the provided index. */ - default boolean hasNext() { - if (getPageable().getMode() == Mode.OFFSET) { - return hasTotalSize() - ? getOffset() + getSize() < getTotalSize() - : getContent().size() == getSize(); - } - return getPageable().hasNext(); + default Optional getCursor(int i) { + return Optional.empty(); } - /** - * Determines whether there is a previous page. - * - * @since 4.8.0 - * @return Whether there exist a previous page. - */ - default boolean hasPrevious() { - if (getPageable().getMode() == Mode.OFFSET) { - return getOffset() > 0; - } - return getPageable().hasPrevious(); + @Override + default boolean hasNext() { + return hasTotalSize() + ? getOffset() + getSize() < getTotalSize() + : getContent().size() == getSize(); } /** @@ -121,22 +116,47 @@ default boolean hasPrevious() { } /** - * Creates a slice from the given content and pageable. + * Creates a page from the given content, pageable and totalSize. + * * @param content The content * @param pageable The pageable * @param totalSize The total size * @param The generic type * @return The slice */ - @JsonCreator @ReflectiveAccess static @NonNull Page of( @JsonProperty("content") @NonNull List content, @JsonProperty("pageable") @NonNull Pageable pageable, - @JsonProperty("totalSize") @Nullable Long totalSize) { + @JsonProperty("totalSize") @Nullable Long totalSize + ) { return new DefaultPage<>(content, pageable, totalSize); } + /** + * Creates a page from the given content, pageable, cursors and totalSize. + * + * @param content The content + * @param pageable The pageable + * @param cursors The cursors for cursored pagination + * @param totalSize The total size + * @param The generic type + * @return The slice + */ + @JsonCreator + @ReflectiveAccess + static @NonNull Page ofCursors( + @JsonProperty("content") @NonNull List content, + @JsonProperty("pageable") @NonNull Pageable pageable, + @JsonProperty("cursors") @Nullable List cursors, + @JsonProperty("totalSize") @Nullable Long totalSize + ) { + if (cursors == null) { + return new DefaultPage<>(content, pageable, totalSize); + } + return new DefaultCursoredPage<>(content, pageable, cursors, totalSize); + } + /** * Creates an empty page object. * @param The generic type diff --git a/data-model/src/main/java/io/micronaut/data/model/Pageable.java b/data-model/src/main/java/io/micronaut/data/model/Pageable.java index 8a95f0d1294..74dc83010fb 100644 --- a/data-model/src/main/java/io/micronaut/data/model/Pageable.java +++ b/data-model/src/main/java/io/micronaut/data/model/Pageable.java @@ -91,28 +91,6 @@ default boolean requestTotal() { return true; } - /** - * Return whether there is a next page as best this pageable could determine. - * Use {@link Page} for more reliable results. - * - * @since 4.8.0 - * @return Whether there is a next page - */ - default boolean hasNext() { - return false; - } - - /** - * Return whether there is a previous page as best this pageable could determine. - * Use {@link Page} for more reliable results. - * - * @since 4.8.0 - * @return Whether there is a previous page. - */ - default boolean hasPrevious() { - return false; - } - /** * Offset in the requested collection. Defaults to zero. * @return offset in the requested collection @@ -283,7 +261,6 @@ default Pageable withoutTotal() { * @param size The size * @param mode The pagination mode * @param cursor The current cursor - * @param nextCursor The next cursor * @param sort The sort * @param requestTotal Whether to query total count * @return The pageable @@ -295,7 +272,6 @@ default Pageable withoutTotal() { @JsonProperty("size") int size, @JsonProperty("mode") @Nullable Mode mode, @JsonProperty("cursor") @Nullable Cursor cursor, - @JsonProperty("nextCursor") @Nullable Cursor nextCursor, @JsonProperty("sort") @Nullable Sort sort, @JsonProperty(value = "requestTotal", defaultValue = "true") boolean requestTotal ) { @@ -303,8 +279,7 @@ default Pageable withoutTotal() { return new DefaultPageable(page, size, sort, requestTotal); } else { return new DefaultCursoredPageable( - size, cursor, nextCursor, mode, page, - sort == null ? UNSORTED : sort, requestTotal + size, cursor, mode, page, sort == null ? UNSORTED : sort, requestTotal ); } } @@ -343,7 +318,7 @@ default Pageable withoutTotal() { if (sort == null) { sort = UNSORTED; } - return new DefaultCursoredPageable(size, cursor, null, Mode.CURSOR_NEXT, page, sort, true); + return new DefaultCursoredPageable(size, cursor, Mode.CURSOR_NEXT, page, sort, true); } /** @@ -360,7 +335,7 @@ default Pageable withoutTotal() { if (sort == null) { sort = UNSORTED; } - return new DefaultCursoredPageable(size, cursor, null, Mode.CURSOR_PREVIOUS, page, sort, true); + return new DefaultCursoredPageable(size, cursor, Mode.CURSOR_PREVIOUS, page, sort, true); } /** diff --git a/data-model/src/main/java/io/micronaut/data/model/Slice.java b/data-model/src/main/java/io/micronaut/data/model/Slice.java index d15d16ec7a4..51f0ab38e8f 100644 --- a/data-model/src/main/java/io/micronaut/data/model/Slice.java +++ b/data-model/src/main/java/io/micronaut/data/model/Slice.java @@ -57,13 +57,38 @@ public interface Slice extends Iterable { @NonNull Pageable getPageable(); /** - * @return The page page + * @return The page number */ default int getPageNumber() { return getPageable().getNumber(); } /** + * Determine whether there is a next page. + * + * @since 4.8.0 + * @return Whether there exist a next page. + */ + default boolean hasNext() { + return getContent().size() == getSize(); + } + + /** + * Determine whether there is a previous page. + * + * @since 4.8.0 + * @return Whether there exist a previous page. + */ + default boolean hasPrevious() { + return getOffset() > 0; + } + + /** + * Create a pageable for querying the next page of data. + *

A pageable may be created even if the end of data was reached to accommodate for + * cases when new data might be added to the repository. Use {@link #hasNext()} to + * verify if you have reached the end.

+ * * @return The next pageable */ default @NonNull Pageable nextPageable() { @@ -71,7 +96,12 @@ default int getPageNumber() { } /** - * @return The previous pageable. + * Create a pageable for querying the previous page of data. + *

A pageable may be created even if the end of data was reached to accommodate for + * cases when new data might be added to the repository. Use {@link #hasPrevious()} to + * verify if you have reached the end.

+ * + * @return The previous pageable */ default @NonNull Pageable previousPageable() { return getPageable().previous(); diff --git a/data-model/src/test/groovy/io/micronaut/data/model/PageSpec.groovy b/data-model/src/test/groovy/io/micronaut/data/model/PageSpec.groovy index 0084707d51c..aed6b0bdce3 100644 --- a/data-model/src/test/groovy/io/micronaut/data/model/PageSpec.groovy +++ b/data-model/src/test/groovy/io/micronaut/data/model/PageSpec.groovy @@ -125,6 +125,13 @@ class PageSpec extends Specification { json == '{"size":3,"number":0,"sort":{},"mode":"OFFSET"}' def deserializedPageable = serdeMapper.readValue(json, Pageable) deserializedPageable == pageable + + when: + def json2 = '{"size":3,"number":0,"sort":{}}' + def deserializedPageable2 = serdeMapper.readValue(json2, Pageable) + + then: + deserializedPageable2 == pageable } void "test serialization and deserialization of a cursored pageable - serde"() { diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/DefaultFindPageInterceptor.java b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/DefaultFindPageInterceptor.java index e6acc4bbc77..97898ea02a8 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/DefaultFindPageInterceptor.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/DefaultFindPageInterceptor.java @@ -23,6 +23,8 @@ import io.micronaut.data.intercept.RepositoryMethodKey; import io.micronaut.data.model.Page; import io.micronaut.data.model.Pageable; +import io.micronaut.data.model.Pageable.Cursor; +import io.micronaut.data.model.Pageable.Mode; import io.micronaut.data.model.runtime.PreparedQuery; import io.micronaut.data.operations.RepositoryOperations; import io.micronaut.data.runtime.operations.internal.sql.DefaultSqlPreparedQuery; @@ -56,10 +58,6 @@ public R intercept(RepositoryMethodKey methodKey, MethodInvocationContext Iterable iterable = operations.findAll(preparedQuery); List results = (List) CollectionUtils.iterableToList(iterable); Pageable pageable = getPageable(context); - if (preparedQuery instanceof DefaultSqlPreparedQuery sqlPreparedQuery) { - pageable = sqlPreparedQuery.updatePageable(results, pageable); - } - Long totalCount = null; if (pageable.requestTotal()) { PreparedQuery countQuery = prepareCountQuery(methodKey, context); @@ -67,7 +65,15 @@ public R intercept(RepositoryMethodKey methodKey, MethodInvocationContext totalCount = n != null ? n.longValue() : null; } - Page page = Page.of(results, pageable, totalCount); + Page page; + if (pageable.getMode() == Mode.OFFSET) { + page = Page.of(results, pageable, totalCount); + } else if (preparedQuery instanceof DefaultSqlPreparedQuery sqlPreparedQuery) { + List cursors = sqlPreparedQuery.createCursors((List) results, pageable); + page = Page.ofCursors(results, pageable, cursors, totalCount); + } else { + throw new UnsupportedOperationException("Only offest pageable mode is supported by this query implementation"); + } if (returnType.isInstance(page)) { return (R) page; } else { diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/DefaultFindSliceInterceptor.java b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/DefaultFindSliceInterceptor.java index a046eaed9b8..1620acf6aa2 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/DefaultFindSliceInterceptor.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/DefaultFindSliceInterceptor.java @@ -27,7 +27,6 @@ import io.micronaut.data.model.runtime.PagedQuery; import io.micronaut.data.model.runtime.PreparedQuery; import io.micronaut.data.operations.RepositoryOperations; -import io.micronaut.data.runtime.operations.internal.sql.DefaultSqlPreparedQuery; import java.util.List; @@ -57,9 +56,6 @@ public R intercept(RepositoryMethodKey methodKey, MethodInvocationContext Pageable pageable = preparedQuery.getPageable(); Iterable iterable = (Iterable) operations.findAll(preparedQuery); List results = CollectionUtils.iterableToList(iterable); - if (preparedQuery instanceof DefaultSqlPreparedQuery sqlPreparedQuery) { - pageable = sqlPreparedQuery.updatePageable(results, pageable); - } Slice slice = Slice.of(results, pageable); return convertOrFail(context, slice); } else { diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/DefaultSqlPreparedQuery.java b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/DefaultSqlPreparedQuery.java index 406d8a66319..262c15c6fe8 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/DefaultSqlPreparedQuery.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/DefaultSqlPreparedQuery.java @@ -23,6 +23,7 @@ import io.micronaut.data.model.DataType; import io.micronaut.data.model.Pageable; import io.micronaut.data.model.Pageable.Cursor; +import io.micronaut.data.model.Pageable.Mode; import io.micronaut.data.model.PersistentProperty; import io.micronaut.data.model.Sort; import io.micronaut.data.model.Sort.Order; @@ -293,34 +294,22 @@ private String buildCursorPagination(@Nullable Pageable.Cursor cursor, @NonNull * @param pageable The pageable sent by user * @return The updated pageable */ - public Pageable updatePageable(List results, Pageable pageable) { - if (pageable instanceof CursoredPageable cursored) { - Cursor cursor = cursored.cursor().orElse(null); - Cursor nextCursor = null; - if (!results.isEmpty()) { - E firstValue = (E) results.get(0); - cursor = Cursor.of(new ArrayList<>(cursorProperties.size())); - for (RuntimePersistentProperty property : cursorProperties) { - cursor.elements().add(property.getProperty().get(firstValue)); - } - if (results.size() == cursored.getSize()) { - E lastValue = (E) results.get(results.size() - 1); - nextCursor = Cursor.of(new ArrayList<>(cursorProperties.size())); - for (RuntimePersistentProperty property : cursorProperties) { - nextCursor.elements().add(property.getProperty().get(lastValue)); - } - } - } - - if (cursored.isBackward()) { - Collections.reverse(results); + public List createCursors(List results, Pageable pageable) { + if (pageable.getMode() != Mode.CURSOR_NEXT && pageable.getMode() != Mode.CURSOR_PREVIOUS) { + return null; + } + if (pageable.getMode() == Mode.CURSOR_PREVIOUS) { + Collections.reverse(results); + } + List cursors = new ArrayList<>(results.size()); + for (Object result: results) { + List cursorElements = new ArrayList<>(cursorProperties.size()); + for (RuntimePersistentProperty property : cursorProperties) { + cursorElements.add(property.getProperty().get((E) result)); } - return CursoredPageable.from( - cursored.getNumber(), cursor, nextCursor, cursored.getMode(), cursored.getSize(), - cursored.getSort(), cursored.requestTotal() - ); + cursors.add(Cursor.of(cursorElements)); } - return pageable; + return cursors; } @Override From 6d261ea249878343ff1b13999ab2f0a3eab5d883 Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Thu, 18 Apr 2024 19:21:18 -0400 Subject: [PATCH 16/20] Slightly improve the test --- .../java/io/micronaut/data/model/DefaultCursoredPage.java | 2 +- .../data/tck/tests/AbstractCursoredPageSpec.groovy | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/data-model/src/main/java/io/micronaut/data/model/DefaultCursoredPage.java b/data-model/src/main/java/io/micronaut/data/model/DefaultCursoredPage.java index 10d9917f6c7..48d3d7491dc 100644 --- a/data-model/src/main/java/io/micronaut/data/model/DefaultCursoredPage.java +++ b/data-model/src/main/java/io/micronaut/data/model/DefaultCursoredPage.java @@ -78,7 +78,7 @@ public boolean equals(Object o) { @Override public Optional getCursor(int i) { - return Optional.of(cursors.get(i)); + return i >= cursors.size() ? Optional.empty() : Optional.of(cursors.get(i)); } @Override diff --git a/data-tck/src/main/groovy/io/micronaut/data/tck/tests/AbstractCursoredPageSpec.groovy b/data-tck/src/main/groovy/io/micronaut/data/tck/tests/AbstractCursoredPageSpec.groovy index 7131e039d5e..a08ffb7fc76 100644 --- a/data-tck/src/main/groovy/io/micronaut/data/tck/tests/AbstractCursoredPageSpec.groovy +++ b/data-tck/src/main/groovy/io/micronaut/data/tck/tests/AbstractCursoredPageSpec.groovy @@ -75,6 +75,8 @@ abstract class AbstractCursoredPageSpec extends Specification { page.content[1].name == name2 page.totalSize == 780 page.totalPages == 78 + page.getCursor(0).isPresent() + page.getCursor(9).isPresent() page.hasNext() when: "The next page is selected" @@ -145,6 +147,8 @@ abstract class AbstractCursoredPageSpec extends Specification { page.pageNumber == 0 page.content[0].name == elem1 page.content.size() == 8 + page.getCursor(7).isPresent() + page.getCursor(8).isEmpty() !page.hasPrevious() page.hasNext() @@ -202,6 +206,8 @@ abstract class AbstractCursoredPageSpec extends Specification { page.pageNumber == 0 page.content[0].name == elem1 page.content[1].name == elem2 + page.getCursor(1).isPresent() + page.getCursor(2).isEmpty() page.content.size() == 2 !page.hasPrevious() From b6278a9944a408e754ddc02a8fe68a10091017f7 Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Tue, 23 Apr 2024 17:00:40 -0400 Subject: [PATCH 17/20] Fix for postgres r2dbc test --- .../r2dbc/postgres/PostgresCursoredPaginationSpec.groovy | 2 +- .../operations/internal/sql/DefaultSqlPreparedQuery.java | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/data-r2dbc/src/test/groovy/io/micronaut/data/r2dbc/postgres/PostgresCursoredPaginationSpec.groovy b/data-r2dbc/src/test/groovy/io/micronaut/data/r2dbc/postgres/PostgresCursoredPaginationSpec.groovy index 1efee4a74ad..5ef146b2e94 100644 --- a/data-r2dbc/src/test/groovy/io/micronaut/data/r2dbc/postgres/PostgresCursoredPaginationSpec.groovy +++ b/data-r2dbc/src/test/groovy/io/micronaut/data/r2dbc/postgres/PostgresCursoredPaginationSpec.groovy @@ -24,7 +24,7 @@ import spock.lang.AutoCleanup import spock.lang.Ignore import spock.lang.Shared -@Ignore("Causes error: 'FATAL: sorry, too many clients already'") +//@Ignore("Causes error: 'FATAL: sorry, too many clients already'") class PostgresCursoredPaginationSpec extends AbstractCursoredPageSpec implements PostgresTestPropertyProvider { @Shared @AutoCleanup ApplicationContext context diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/DefaultSqlPreparedQuery.java b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/DefaultSqlPreparedQuery.java index 262c15c6fe8..c170079d383 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/DefaultSqlPreparedQuery.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/DefaultSqlPreparedQuery.java @@ -261,6 +261,8 @@ private String buildCursorPagination(@Nullable Pageable.Cursor cursor, @NonNull } else { builder.append("WHERE ("); } + String positionalParameter = getQueryBuilder().positionalParameterFormat(); + int paramIndex = storedQuery.getQueryBindings().size() + 1; for (int i = 0; i < orders.size(); ++i) { builder.append("("); for (int j = 0; j <= i; ++j) { @@ -272,7 +274,7 @@ private String buildCursorPagination(@Nullable Pageable.Cursor cursor, @NonNull builder.append(i == j ? " < " : " = "); } cursorQueryBindings.add(cursorBindings.get(j)); - builder.append("?"); + builder.append(String.format(positionalParameter, paramIndex++)); if (i != j) { builder.append(" AND "); } @@ -293,7 +295,9 @@ private String buildCursorPagination(@Nullable Pageable.Cursor cursor, @NonNull * @param results The scanning results * @param pageable The pageable sent by user * @return The updated pageable + * @since 4.8.0 */ + @Internal public List createCursors(List results, Pageable pageable) { if (pageable.getMode() != Mode.CURSOR_NEXT && pageable.getMode() != Mode.CURSOR_PREVIOUS) { return null; From 8aa2078999a3081b7e400682b8aed9f3b8bb1fbd Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Tue, 23 Apr 2024 17:08:11 -0400 Subject: [PATCH 18/20] Add CursoredPage and interceptor --- .../annotation/RepositoryConfiguration.java | 4 +- .../micronaut/data/annotation/TypeRole.java | 5 + .../FindCursoredPageInterceptor.java | 27 +++ .../io/micronaut/data/model/CursoredPage.java | 182 ++++++++++++++++++ .../data/model/CursoredPageable.java | 18 +- .../data/model/DefaultCursoredPage.java | 38 +--- .../java/io/micronaut/data/model/Page.java | 19 +- .../io/micronaut/data/model/Pageable.java | 4 +- .../data/model/runtime/PagedQuery.java | 1 + .../RepositoryTypeElementVisitor.java | 7 +- .../visitors/finders/FindersUtils.java | 13 +- .../criteria/QueryCriteriaMethodMatch.java | 4 +- .../data/processor/visitors/PageSpec.groovy | 38 ++++ .../DefaultFindCursoredPageInterceptor.java | 57 ++++++ .../intercept/DefaultFindPageInterceptor.java | 5 +- .../tck/tests/AbstractCursoredPageSpec.groovy | 17 +- .../tck/repositories/PersonRepository.java | 4 + 17 files changed, 361 insertions(+), 82 deletions(-) create mode 100644 data-model/src/main/java/io/micronaut/data/intercept/FindCursoredPageInterceptor.java create mode 100644 data-model/src/main/java/io/micronaut/data/model/CursoredPage.java create mode 100644 data-runtime/src/main/java/io/micronaut/data/runtime/intercept/DefaultFindCursoredPageInterceptor.java diff --git a/data-model/src/main/java/io/micronaut/data/annotation/RepositoryConfiguration.java b/data-model/src/main/java/io/micronaut/data/annotation/RepositoryConfiguration.java index 24bcb44b8d9..268a146319c 100644 --- a/data-model/src/main/java/io/micronaut/data/annotation/RepositoryConfiguration.java +++ b/data-model/src/main/java/io/micronaut/data/annotation/RepositoryConfiguration.java @@ -15,6 +15,7 @@ */ package io.micronaut.data.annotation; +import io.micronaut.data.model.CursoredPage; import io.micronaut.data.model.Page; import io.micronaut.data.model.Pageable; import io.micronaut.data.model.Slice; @@ -59,7 +60,8 @@ TypeRole[] typeRoles() default { @TypeRole(role = TypeRole.PAGEABLE, type = Pageable.class), @TypeRole(role = TypeRole.SORT, type = Sort.class), @TypeRole(role = TypeRole.SLICE, type = Slice.class), - @TypeRole(role = TypeRole.PAGE, type = Page.class) + @TypeRole(role = TypeRole.PAGE, type = Page.class), + @TypeRole(role = TypeRole.CURSORED_PAGE, type = CursoredPage.class) }; /** diff --git a/data-model/src/main/java/io/micronaut/data/annotation/TypeRole.java b/data-model/src/main/java/io/micronaut/data/annotation/TypeRole.java index efaa389a871..810510209a0 100644 --- a/data-model/src/main/java/io/micronaut/data/annotation/TypeRole.java +++ b/data-model/src/main/java/io/micronaut/data/annotation/TypeRole.java @@ -75,6 +75,11 @@ */ String PAGE = "page"; + /** + * The parameter that is used to represent a {@link io.micronaut.data.model.CursoredPage}. + */ + String CURSORED_PAGE = "cursoredPage"; + /** * The name of the role. * @return The role name diff --git a/data-model/src/main/java/io/micronaut/data/intercept/FindCursoredPageInterceptor.java b/data-model/src/main/java/io/micronaut/data/intercept/FindCursoredPageInterceptor.java new file mode 100644 index 00000000000..3f287398ab0 --- /dev/null +++ b/data-model/src/main/java/io/micronaut/data/intercept/FindCursoredPageInterceptor.java @@ -0,0 +1,27 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.data.intercept; + +/** + * An interceptor that handles a return type of {@link io.micronaut.data.model.CursoredPage}. + * + * @author Andriy Dmytruk + * @param The declaring type + * @param The return type + * @since 4.8.0 + */ +public interface FindCursoredPageInterceptor extends DataInterceptor { +} diff --git a/data-model/src/main/java/io/micronaut/data/model/CursoredPage.java b/data-model/src/main/java/io/micronaut/data/model/CursoredPage.java new file mode 100644 index 00000000000..5d2b63df38c --- /dev/null +++ b/data-model/src/main/java/io/micronaut/data/model/CursoredPage.java @@ -0,0 +1,182 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.data.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import io.micronaut.context.annotation.DefaultImplementation; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.annotation.ReflectiveAccess; +import io.micronaut.core.annotation.TypeHint; +import io.micronaut.data.model.Pageable.Cursor; +import io.micronaut.data.model.Pageable.Mode; +import io.micronaut.serde.annotation.Serdeable; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * Inspired by the Jakarta's {@code CursoredPage}, this models a type that supports + * pagination operations with cursors. + * + *

A CursoredPage is a result set associated with a particular {@link Pageable} that includes + * a calculation of the total size of page of records.

+ * + * @param The generic type + * @author Andriy Dmytruk + * @since 4.8.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@TypeHint(CursoredPage.class) +@JsonDeserialize(as = DefaultCursoredPage.class) +@Serdeable +@DefaultImplementation(DefaultCursoredPage.class) +public interface CursoredPage extends Page { + + CursoredPage EMPTY = new DefaultCursoredPage<>(Collections.emptyList(), Pageable.unpaged(), Collections.emptyList(), null); + + /** + * @return Whether this {@link CursoredPage} contains the total count of the records + * @since 4.8.0 + */ + boolean hasTotalSize(); + + /** + * Get the total count of all the records that can be given by this query. + * The method may produce a {@link IllegalStateException} if the {@link Pageable} request + * did not ask for total size. + * + * @return The total size of the all records. + */ + long getTotalSize(); + + /** + * Get the total count of pages that can be given by this query. + * The method may produce a {@link IllegalStateException} if the {@link Pageable} request + * did not ask for total size. + * + * @return The total page of pages + */ + default int getTotalPages() { + int size = getSize(); + return size == 0 ? 1 : (int) Math.ceil((double) getTotalSize() / (double) size); + } + + @Override + default boolean hasNext() { + Pageable pageable = getPageable(); + if (pageable.getMode() == Mode.CURSOR_NEXT) { + return getContent().size() == pageable.getSize(); + } else { + return true; + } + } + + @Override + default boolean hasPrevious() { + Pageable pageable = getPageable(); + if (pageable.getMode() == Mode.CURSOR_PREVIOUS) { + return getContent().size() == pageable.getSize(); + } else { + return true; + } + } + + @Override + default CursoredPageable nextPageable() { + Pageable pageable = getPageable(); + Cursor cursor = getCursor(getCursors().size() - 1).orElse(pageable.cursor().orElse(null)); + return Pageable.afterCursor(cursor, pageable.getNumber() + 1, pageable.getSize(), pageable.getSort()); + } + + @Override + default CursoredPageable previousPageable() { + Pageable pageable = getPageable(); + Cursor cursor = getCursor(0).orElse(pageable.cursor().orElse(null)); + return Pageable.beforeCursor(cursor, Math.max(0, pageable.getNumber() - 1), pageable.getSize(), pageable.getSort()); + } + + + /** + * Maps the content with the given function. + * + * @param function The function to apply to each element in the content. + * @param The type returned by the function + * @return A new slice with the mapped content + */ + @Override + default @NonNull CursoredPage map(Function function) { + List content = getContent().stream().map(function).collect(Collectors.toList()); + return new DefaultCursoredPage<>(content, getPageable(), getCursors(), getTotalSize()); + } + + /** + * Creates a cursored page from the given content, pageable, cursors and totalSize. + * + * @param content The content + * @param pageable The pageable + * @param cursors The cursors for cursored pagination + * @param totalSize The total size + * @param The generic type + * @return The slice + */ + @JsonCreator + @ReflectiveAccess + static @NonNull CursoredPage of( + @JsonProperty("content") @NonNull List content, + @JsonProperty("pageable") @NonNull Pageable pageable, + @JsonProperty("cursors") @Nullable List cursors, + @JsonProperty("totalSize") @Nullable Long totalSize + ) { + return new DefaultCursoredPage<>(content, pageable, cursors, totalSize); + } + + /** + * Get cursor at the given position or empty if no such cursor exists. + * There must be a cursor for each of the data entities in the same order. + * To start pagination after or before a cursor create a pageable from it using the + * same sorting as before. + * + * @param i The index of cursor to retrieve. + * @return The cursor at the provided index. + */ + Optional getCursor(int i); + + /** + * Get all the cursors. + * + * @see #getCursor(int) getCursor(i) for more details. + * @return All the cursors + */ + List getCursors(); + + /** + * Creates an empty page object. + * @param The generic type + * @return The slice + */ + @SuppressWarnings("unchecked") + static @NonNull CursoredPage empty() { + return (CursoredPage) EMPTY; + } + +} diff --git a/data-model/src/main/java/io/micronaut/data/model/CursoredPageable.java b/data-model/src/main/java/io/micronaut/data/model/CursoredPageable.java index fe98ef8a6ce..53bef1495bb 100644 --- a/data-model/src/main/java/io/micronaut/data/model/CursoredPageable.java +++ b/data-model/src/main/java/io/micronaut/data/model/CursoredPageable.java @@ -25,7 +25,7 @@ import io.micronaut.serde.annotation.Serdeable; /** - * Models pageable data that uses a currentCursor. + * Models a pageable request that uses a cursor. * * @author Andriy Dmytruk * @since 4.8.0 @@ -35,13 +35,6 @@ @JsonIgnoreProperties(ignoreUnknown = true) public interface CursoredPageable extends Pageable { - /** - * Constant for no pagination. - */ - CursoredPageable UNPAGED = new DefaultCursoredPageable( - -1, null, Mode.CURSOR_NEXT, 0, Sort.UNSORTED, true - ); - /** * Whether the pageable is traversing backwards. * @@ -62,7 +55,7 @@ default Mode getMode() { */ static @NonNull CursoredPageable from(Sort sort) { if (sort == null) { - return UNPAGED; + sort = Sort.UNSORTED; } return new DefaultCursoredPageable( -1, null, Mode.CURSOR_NEXT, 0, sort, true @@ -113,11 +106,4 @@ default Mode getMode() { return new DefaultCursoredPageable(size, cursor, mode, page, sort, requestTotal); } - /** - * @return A new instance without paging data. - */ - static @NonNull CursoredPageable unpaged() { - return UNPAGED; - } - } diff --git a/data-model/src/main/java/io/micronaut/data/model/DefaultCursoredPage.java b/data-model/src/main/java/io/micronaut/data/model/DefaultCursoredPage.java index 48d3d7491dc..423eca438e2 100644 --- a/data-model/src/main/java/io/micronaut/data/model/DefaultCursoredPage.java +++ b/data-model/src/main/java/io/micronaut/data/model/DefaultCursoredPage.java @@ -20,7 +20,6 @@ import io.micronaut.core.annotation.Creator; import io.micronaut.core.annotation.ReflectiveAccess; import io.micronaut.data.model.Pageable.Cursor; -import io.micronaut.data.model.Pageable.Mode; import io.micronaut.serde.annotation.Serdeable; import java.util.List; @@ -35,7 +34,7 @@ * @param The generic type */ @Serdeable -class DefaultCursoredPage extends DefaultPage { +class DefaultCursoredPage extends DefaultPage implements CursoredPage { private final List cursors; @@ -78,41 +77,12 @@ public boolean equals(Object o) { @Override public Optional getCursor(int i) { - return i >= cursors.size() ? Optional.empty() : Optional.of(cursors.get(i)); + return i >= cursors.size() || i < 0 ? Optional.empty() : Optional.of(cursors.get(i)); } @Override - public boolean hasNext() { - Pageable pageable = getPageable(); - if (pageable.getMode() == Mode.CURSOR_NEXT) { - return cursors.size() == pageable.getSize(); - } else { - return true; - } - } - - @Override - public boolean hasPrevious() { - Pageable pageable = getPageable(); - if (pageable.getMode() == Mode.CURSOR_PREVIOUS) { - return cursors.size() == pageable.getSize(); - } else { - return true; - } - } - - @Override - public Pageable nextPageable() { - Pageable pageable = getPageable(); - Cursor cursor = cursors.isEmpty() ? pageable.cursor().orElse(null) : cursors.get(cursors.size() - 1); - return Pageable.afterCursor(cursor, pageable.getNumber() + 1, pageable.getSize(), pageable.getSort()); - } - - @Override - public Pageable previousPageable() { - Pageable pageable = getPageable(); - Cursor cursor = cursors.isEmpty() ? pageable.cursor().orElse(null) : cursors.get(0); - return Pageable.beforeCursor(cursor, Math.max(0, pageable.getNumber() - 1), pageable.getSize(), pageable.getSort()); + public List getCursors() { + return cursors; } @Override diff --git a/data-model/src/main/java/io/micronaut/data/model/Page.java b/data-model/src/main/java/io/micronaut/data/model/Page.java index 107d92429a0..ac49597b029 100644 --- a/data-model/src/main/java/io/micronaut/data/model/Page.java +++ b/data-model/src/main/java/io/micronaut/data/model/Page.java @@ -20,6 +20,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import io.micronaut.context.annotation.DefaultImplementation; +import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.annotation.ReflectiveAccess; @@ -29,7 +30,6 @@ import java.util.Collections; import java.util.List; -import java.util.Optional; import java.util.function.Function; import java.util.stream.Collectors; @@ -80,21 +80,6 @@ default int getTotalPages() { return size == 0 ? 1 : (int) Math.ceil((double) getTotalSize() / (double) size); } - /** - * Get cursor at the given position. - * Cursors are only available if {@code getPageable().getMode()} is one of - * {@link Pageable.Mode#CURSOR_NEXT} or {@link Pageable.Mode#CURSOR_PREVIOUS}. - * In that case there is a cursor for each of the data entities in the same order. - * To start pagination after or before a cursor create a pageable from it using the - * same sorting as before. - * - * @param i The index of cursor to retrieve. - * @return The cursor at the provided index. - */ - default Optional getCursor(int i) { - return Optional.empty(); - } - @Override default boolean hasNext() { return hasTotalSize() @@ -135,6 +120,7 @@ default boolean hasNext() { /** * Creates a page from the given content, pageable, cursors and totalSize. + * This method is for JSON deserialization. Please use {@link CursoredPage#of} instead. * * @param content The content * @param pageable The pageable @@ -144,6 +130,7 @@ default boolean hasNext() { * @return The slice */ @JsonCreator + @Internal @ReflectiveAccess static @NonNull Page ofCursors( @JsonProperty("content") @NonNull List content, diff --git a/data-model/src/main/java/io/micronaut/data/model/Pageable.java b/data-model/src/main/java/io/micronaut/data/model/Pageable.java index 74dc83010fb..70ea4c294c6 100644 --- a/data-model/src/main/java/io/micronaut/data/model/Pageable.java +++ b/data-model/src/main/java/io/micronaut/data/model/Pageable.java @@ -314,7 +314,7 @@ default Pageable withoutTotal() { * @param sort The sorting * @return The pageable */ - static @NonNull Pageable afterCursor(@NonNull Cursor cursor, int page, int size, @Nullable Sort sort) { + static @NonNull CursoredPageable afterCursor(@NonNull Cursor cursor, int page, int size, @Nullable Sort sort) { if (sort == null) { sort = UNSORTED; } @@ -331,7 +331,7 @@ default Pageable withoutTotal() { * @param sort The sorting * @return The pageable */ - static @NonNull Pageable beforeCursor(@NonNull Cursor cursor, int page, int size, @Nullable Sort sort) { + static @NonNull CursoredPageable beforeCursor(@NonNull Cursor cursor, int page, int size, @Nullable Sort sort) { if (sort == null) { sort = UNSORTED; } diff --git a/data-model/src/main/java/io/micronaut/data/model/runtime/PagedQuery.java b/data-model/src/main/java/io/micronaut/data/model/runtime/PagedQuery.java index 341beabe7a4..4674f7360cd 100644 --- a/data-model/src/main/java/io/micronaut/data/model/runtime/PagedQuery.java +++ b/data-model/src/main/java/io/micronaut/data/model/runtime/PagedQuery.java @@ -53,4 +53,5 @@ public interface PagedQuery extends Named, AnnotationMetadataProvider { default Map getQueryHints() { return Collections.emptyMap(); } + } diff --git a/data-processor/src/main/java/io/micronaut/data/processor/visitors/RepositoryTypeElementVisitor.java b/data-processor/src/main/java/io/micronaut/data/processor/visitors/RepositoryTypeElementVisitor.java index 8e6ff37198b..9d302802ec7 100644 --- a/data-processor/src/main/java/io/micronaut/data/processor/visitors/RepositoryTypeElementVisitor.java +++ b/data-processor/src/main/java/io/micronaut/data/processor/visitors/RepositoryTypeElementVisitor.java @@ -38,6 +38,7 @@ import io.micronaut.data.annotation.sql.Procedure; import io.micronaut.data.intercept.annotation.DataMethod; import io.micronaut.data.intercept.annotation.DataMethodQueryParameter; +import io.micronaut.data.model.CursoredPage; import io.micronaut.data.model.DataType; import io.micronaut.data.model.JsonDataType; import io.micronaut.data.model.Page; @@ -121,6 +122,7 @@ public class RepositoryTypeElementVisitor implements TypeElementVisitor count = cb.createQuery(); // count.select(cb.count(query.getRoots().iterator().next())); // CommonAbstractCriteria countQueryCriteria = defineQuery(matchContext, matchContext.getRootEntity(), cb); diff --git a/data-processor/src/test/groovy/io/micronaut/data/processor/visitors/PageSpec.groovy b/data-processor/src/test/groovy/io/micronaut/data/processor/visitors/PageSpec.groovy index 173532f8913..2384b976c3c 100644 --- a/data-processor/src/test/groovy/io/micronaut/data/processor/visitors/PageSpec.groovy +++ b/data-processor/src/test/groovy/io/micronaut/data/processor/visitors/PageSpec.groovy @@ -19,8 +19,10 @@ import io.micronaut.annotation.processing.TypeElementVisitorProcessor import io.micronaut.annotation.processing.test.JavaParser import io.micronaut.data.annotation.Join import io.micronaut.data.annotation.Query +import io.micronaut.data.intercept.FindCursoredPageInterceptor import io.micronaut.data.intercept.FindPageInterceptor import io.micronaut.data.intercept.annotation.DataMethod +import io.micronaut.data.model.CursoredPageable import io.micronaut.data.model.Pageable import io.micronaut.data.model.PersistentEntity import io.micronaut.data.model.entities.Person @@ -266,6 +268,42 @@ interface MyInterface extends GenericRepository { e.message.contains('Query returns a Page and does not specify a \'countQuery\' member') } + void "test cursored page method match"() { + given: + BeanDefinition beanDefinition = buildRepository('test.MyInterface' , """ + +import io.micronaut.context.annotation.Executable; +import io.micronaut.data.model.entities.Person; + +@Repository +@Executable +interface MyInterface extends GenericRepository { + + CursoredPage list(Pageable pageable); + + CursoredPage findByName(String title, Pageable pageable); + +} +""") + + def alias = new JpaQueryBuilder().getAliasName(PersistentEntity.of(Person)) + + when: "the list method is retrieved" + def listMethod = beanDefinition.getRequiredMethod("list", Pageable) + def listAnn = listMethod.synthesize(DataMethod) + + def findMethod = beanDefinition.getRequiredMethod("findByName", String, Pageable) + def findAnn = findMethod.synthesize(DataMethod) + + + then:"it is configured correctly" + listAnn.interceptor() == FindCursoredPageInterceptor + findAnn.interceptor() == FindCursoredPageInterceptor + findMethod.hasAnnotation(Query.class) + findMethod.getValue(Query.class, "value", String).get() == "SELECT $alias FROM io.micronaut.data.model.entities.Person AS $alias WHERE (${alias}.name = :p1)" + findMethod.getValue(Query.class, "countQuery", String).get() == "SELECT COUNT($alias) FROM io.micronaut.data.model.entities.Person AS $alias WHERE (${alias}.name = :p1)" + } + @Override protected JavaParser newJavaParser() { return new JavaParser() { diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/DefaultFindCursoredPageInterceptor.java b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/DefaultFindCursoredPageInterceptor.java new file mode 100644 index 00000000000..37ef89a3570 --- /dev/null +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/DefaultFindCursoredPageInterceptor.java @@ -0,0 +1,57 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.data.runtime.intercept; + +import io.micronaut.aop.MethodInvocationContext; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.data.intercept.FindCursoredPageInterceptor; +import io.micronaut.data.model.CursoredPageable; +import io.micronaut.data.model.Pageable; +import io.micronaut.data.model.Pageable.Mode; +import io.micronaut.data.operations.RepositoryOperations; + +/** + * Default implementation of {@link FindCursoredPageInterceptor}. + * + * @param The declaring type + * @param The paged type. + * @author Andriy Dmytruk + * @since 4.8.0 + */ +public class DefaultFindCursoredPageInterceptor extends DefaultFindPageInterceptor implements FindCursoredPageInterceptor { + + /** + * Default constructor. + * + * @param datastore The operations + */ + protected DefaultFindCursoredPageInterceptor(@NonNull RepositoryOperations datastore) { + super(datastore); + } + + @Override + protected Pageable getPageable(MethodInvocationContext context) { + Pageable pageable = super.getPageable(context); + if (pageable.getMode() == Mode.OFFSET) { + if (pageable.getNumber() == 0) { + pageable = CursoredPageable.from(pageable.getSize(), pageable.getSort()); + } else { + throw new IllegalArgumentException("Pageable with offset mode provided, but method must return a cursored page"); + } + } + return pageable; + } +} diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/DefaultFindPageInterceptor.java b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/DefaultFindPageInterceptor.java index 97898ea02a8..40268322a51 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/DefaultFindPageInterceptor.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/DefaultFindPageInterceptor.java @@ -21,6 +21,7 @@ import io.micronaut.data.annotation.Query; import io.micronaut.data.intercept.FindPageInterceptor; import io.micronaut.data.intercept.RepositoryMethodKey; +import io.micronaut.data.model.CursoredPage; import io.micronaut.data.model.Page; import io.micronaut.data.model.Pageable; import io.micronaut.data.model.Pageable.Cursor; @@ -70,9 +71,9 @@ public R intercept(RepositoryMethodKey methodKey, MethodInvocationContext page = Page.of(results, pageable, totalCount); } else if (preparedQuery instanceof DefaultSqlPreparedQuery sqlPreparedQuery) { List cursors = sqlPreparedQuery.createCursors((List) results, pageable); - page = Page.ofCursors(results, pageable, cursors, totalCount); + page = CursoredPage.of(results, pageable, cursors, totalCount); } else { - throw new UnsupportedOperationException("Only offest pageable mode is supported by this query implementation"); + throw new UnsupportedOperationException("Only offset pageable mode is supported by this query implementation"); } if (returnType.isInstance(page)) { return (R) page; diff --git a/data-tck/src/main/groovy/io/micronaut/data/tck/tests/AbstractCursoredPageSpec.groovy b/data-tck/src/main/groovy/io/micronaut/data/tck/tests/AbstractCursoredPageSpec.groovy index a08ffb7fc76..a8a967793f3 100644 --- a/data-tck/src/main/groovy/io/micronaut/data/tck/tests/AbstractCursoredPageSpec.groovy +++ b/data-tck/src/main/groovy/io/micronaut/data/tck/tests/AbstractCursoredPageSpec.groovy @@ -17,6 +17,7 @@ package io.micronaut.data.tck.tests import io.micronaut.data.model.CursoredPageable import io.micronaut.data.model.Page +import io.micronaut.data.model.Pageable import io.micronaut.data.model.Sort import io.micronaut.data.tck.entities.Book import io.micronaut.data.tck.entities.Person @@ -115,8 +116,8 @@ abstract class AbstractCursoredPageSpec extends Specification { void "test pageable list with row removal"() { when: "10 people are paged" - def pageable = CursoredPageable.from(10, sorting) - Page page = personRepository.findAll(pageable) + def pageable = Pageable.from(0, 10, sorting) // The first pageable can be non-cursored + Page page = personRepository.retrieve(pageable) // The retrieve method explicitly returns CursoredPage then: "The data is correct" page.content.size() == 10 @@ -127,7 +128,7 @@ abstract class AbstractCursoredPageSpec extends Specification { when: "The next page is selected after deletion" personRepository.delete(page.content[1]) personRepository.delete(page.content[9]) - page = personRepository.findAll(page.nextPageable()) + page = personRepository.retrieve(page.nextPageable()) then: "it is correct" page.offset == 10 @@ -140,7 +141,7 @@ abstract class AbstractCursoredPageSpec extends Specification { when: "The previous page is selected" pageable = page.previousPageable() - page = personRepository.findAll(pageable) + page = personRepository.retrieve(pageable) then: "it is correct" page.offset == 0 @@ -163,7 +164,7 @@ abstract class AbstractCursoredPageSpec extends Specification { void "test pageable list with row addition"() { when: "10 people are paged" def pageable = CursoredPageable.from(10, sorting) - Page page = personRepository.findAll(pageable) + Page page = personRepository.retrieve(pageable) then: "The data is correct" page.content.size() == 10 @@ -176,7 +177,7 @@ abstract class AbstractCursoredPageSpec extends Specification { new Person(name: "AAAAA00"), new Person(name: "AAAAA01"), new Person(name: "ZZZZZ08"), new Person(name: "ZZZZZ07") ]) - page = personRepository.findAll(page.nextPageable()) + page = personRepository.retrieve(page.nextPageable()) then: "it is correct" page.offset == 10 @@ -189,7 +190,7 @@ abstract class AbstractCursoredPageSpec extends Specification { when: "The previous page is selected" pageable = page.previousPageable() - page = personRepository.findAll(pageable) + page = personRepository.retrieve(pageable) then: "it is correct" page.offset == 0 @@ -199,7 +200,7 @@ abstract class AbstractCursoredPageSpec extends Specification { page.hasPrevious() when: "The second previous page is selected" - page = personRepository.findAll(page.previousPageable()) + page = personRepository.retrieve(page.previousPageable()) then: page.offset == 0 diff --git a/data-tck/src/main/java/io/micronaut/data/tck/repositories/PersonRepository.java b/data-tck/src/main/java/io/micronaut/data/tck/repositories/PersonRepository.java index f875d37e573..617f2bc48da 100644 --- a/data-tck/src/main/java/io/micronaut/data/tck/repositories/PersonRepository.java +++ b/data-tck/src/main/java/io/micronaut/data/tck/repositories/PersonRepository.java @@ -16,10 +16,12 @@ package io.micronaut.data.tck.repositories; import io.micronaut.context.annotation.Parameter; +import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.data.annotation.Id; import io.micronaut.data.annotation.ParameterExpression; import io.micronaut.data.annotation.Query; +import io.micronaut.data.model.CursoredPage; import io.micronaut.data.model.Page; import io.micronaut.data.model.Pageable; import io.micronaut.data.model.Slice; @@ -173,6 +175,8 @@ public interface PersonRepository extends CrudRepository, Pageable List findDistinctName(); + CursoredPage retrieve(@NonNull Pageable pageable); + class Specifications { public static PredicateSpecification nameEquals(String name) { From 0ccf54a09b8cabf5b5963bd83c8dda7570b87c0c Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Wed, 24 Apr 2024 14:16:50 -0400 Subject: [PATCH 19/20] Add documentation for cursored pageable --- .../main/groovy/example/BookRepository.groovy | 8 +++ .../groovy/example/BookRepositorySpec.groovy | 36 +++++++++++++ .../src/main/java/example/BookRepository.java | 8 +++ .../test/java/example/BookRepositorySpec.java | 45 +++++++++++++++++ .../src/main/kotlin/example/BookRepository.kt | 12 +++-- .../test/kotlin/example/BookRepositorySpec.kt | 50 +++++++++++++++++-- .../shared/querying/cursored-pagination.adoc | 22 ++++++++ src/main/docs/guide/toc.yml | 1 + 8 files changed, 176 insertions(+), 6 deletions(-) create mode 100644 src/main/docs/guide/shared/querying/cursored-pagination.adoc diff --git a/doc-examples/jdbc-example-groovy/src/main/groovy/example/BookRepository.groovy b/doc-examples/jdbc-example-groovy/src/main/groovy/example/BookRepository.groovy index 6aebd45bf09..f01a1294715 100644 --- a/doc-examples/jdbc-example-groovy/src/main/groovy/example/BookRepository.groovy +++ b/doc-examples/jdbc-example-groovy/src/main/groovy/example/BookRepository.groovy @@ -46,6 +46,14 @@ interface BookRepository extends CrudRepository { // <2> Slice list(Pageable pageable); // end::pageable[] + // tag::cursored-pageable[] + CursoredPage find(CursoredPageable pageable) // <1> + + CursoredPage findByPagesBetween(int minPageCount, int maxPageCount, Pageable pageable) // <2> + + Page findByTitleStartingWith(String title, Pageable pageable) // <3> + // end::cursored-pageable[] + // tag::simple-projection[] List findTitleByPagesGreaterThan(int pageCount); // end::simple-projection[] diff --git a/doc-examples/jdbc-example-groovy/src/test/groovy/example/BookRepositorySpec.groovy b/doc-examples/jdbc-example-groovy/src/test/groovy/example/BookRepositorySpec.groovy index 722116101d2..c1699f5c4c9 100644 --- a/doc-examples/jdbc-example-groovy/src/test/groovy/example/BookRepositorySpec.groovy +++ b/doc-examples/jdbc-example-groovy/src/test/groovy/example/BookRepositorySpec.groovy @@ -1,5 +1,10 @@ package example +import io.micronaut.data.model.CursoredPage +import io.micronaut.data.model.CursoredPageable +import io.micronaut.data.model.Page +import io.micronaut.data.model.Pageable +import io.micronaut.data.model.Sort import io.micronaut.test.extensions.spock.annotation.MicronautTest import spock.lang.Shared import spock.lang.Specification @@ -53,5 +58,36 @@ class BookRepositorySpec extends Specification { bookRepository.count() == 0 } + void "test cursored pageable"() { + given: + bookRepository.saveAll(Arrays.asList( + new Book("The Stand", 1000), + new Book("The Shining", 600), + new Book("The Power of the Dog", 500), + new Book("The Border", 700), + new Book("Along Came a Spider", 300), + new Book("Pet Cemetery", 400), + new Book("A Game of Thrones", 900), + new Book("A Clash of Kings", 1100) + )) + + when: + // tag::cursored-pageable[] + CursoredPage page = // <1> + bookRepository.find(CursoredPageable.from(5, Sort.of(Sort.Order.asc("title")))) + CursoredPage page2 = bookRepository.find(page.nextPageable()) // <2> + CursoredPage pageByPagesBetween = // <3> + bookRepository.findByPagesBetween(400, 700, Pageable.from(0, 3)) + Page pageByTitleStarts = // <4> + bookRepository.findByTitleStartingWith("The", CursoredPageable.from( 3, Sort.unsorted())) + // end::cursored-pageable[] + + then: + page.getNumberOfElements() == 5 + page2.getNumberOfElements() == 3 + pageByPagesBetween.getNumberOfElements() == 3 + pageByTitleStarts.getNumberOfElements() == 3 + } + } diff --git a/doc-examples/jdbc-example-java/src/main/java/example/BookRepository.java b/doc-examples/jdbc-example-java/src/main/java/example/BookRepository.java index 4a24052d3bc..d88b4c2130f 100644 --- a/doc-examples/jdbc-example-java/src/main/java/example/BookRepository.java +++ b/doc-examples/jdbc-example-java/src/main/java/example/BookRepository.java @@ -46,6 +46,14 @@ interface BookRepository extends CrudRepository { // <2> Slice list(Pageable pageable); // end::pageable[] + // tag::cursored-pageable[] + CursoredPage find(CursoredPageable pageable); // <1> + + CursoredPage findByPagesBetween(int minPageCount, int maxPageCount, Pageable pageable); // <2> + + Page findByTitleStartingWith(String title, Pageable pageable); // <3> + // end::cursored-pageable[] + // tag::simple-projection[] List findTitleByPagesGreaterThan(int pageCount); // end::simple-projection[] diff --git a/doc-examples/jdbc-example-java/src/test/java/example/BookRepositorySpec.java b/doc-examples/jdbc-example-java/src/test/java/example/BookRepositorySpec.java index 57751dcab6c..3d09ec45cba 100644 --- a/doc-examples/jdbc-example-java/src/test/java/example/BookRepositorySpec.java +++ b/doc-examples/jdbc-example-java/src/test/java/example/BookRepositorySpec.java @@ -2,6 +2,8 @@ import io.micronaut.context.BeanContext; import io.micronaut.data.annotation.Query; +import io.micronaut.data.model.CursoredPage; +import io.micronaut.data.model.CursoredPageable; import io.micronaut.data.model.Page; import io.micronaut.data.model.Pageable; import io.micronaut.data.model.Slice; @@ -9,6 +11,8 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import io.micronaut.data.model.Sort; +import io.micronaut.data.model.Sort.Order; import jakarta.inject.Inject; import io.micronaut.test.extensions.junit5.annotation.MicronautTest; @@ -133,6 +137,47 @@ void testPageable() { assertEquals(1, results.size()); } + @Test + void testCursoredPageable() { + bookRepository.saveAll(Arrays.asList( + new Book("The Stand", 1000), + new Book("The Shining", 600), + new Book("The Power of the Dog", 500), + new Book("The Border", 700), + new Book("Along Came a Spider", 300), + new Book("Pet Cemetery", 400), + new Book("A Game of Thrones", 900), + new Book("A Clash of Kings", 1100) + )); + + // tag::cursored-pageable[] + CursoredPage page = // <1> + bookRepository.find(CursoredPageable.from(5, Sort.of(Order.asc("title")))); + CursoredPage page2 = bookRepository.find(page.nextPageable()); // <2> + CursoredPage pageByPagesBetween = // <3> + bookRepository.findByPagesBetween(400, 700, Pageable.from(0, 3)); + Page pageByTitleStarts = // <4> + bookRepository.findByTitleStartingWith("The", CursoredPageable.from( 3, Sort.unsorted())); + // end::cursored-pageable[] + + assertEquals( + 5, + page.getNumberOfElements() + ); + assertEquals( + 3, + page2.getNumberOfElements() + ); + assertEquals( + 3, + pageByPagesBetween.getNumberOfElements() + ); + assertEquals( + 3, + pageByTitleStarts.getNumberOfElements() + ); + } + @Test void testDto() { bookRepository.save(new Book("The Shining", 400)); diff --git a/doc-examples/jdbc-example-kotlin/src/main/kotlin/example/BookRepository.kt b/doc-examples/jdbc-example-kotlin/src/main/kotlin/example/BookRepository.kt index 8e5e9f646d0..8437e37ce39 100644 --- a/doc-examples/jdbc-example-kotlin/src/main/kotlin/example/BookRepository.kt +++ b/doc-examples/jdbc-example-kotlin/src/main/kotlin/example/BookRepository.kt @@ -8,9 +8,7 @@ import io.micronaut.data.annotation.Id import io.micronaut.data.annotation.Query import io.micronaut.data.annotation.sql.Procedure import io.micronaut.data.jdbc.annotation.JdbcRepository -import io.micronaut.data.model.Page -import io.micronaut.data.model.Pageable -import io.micronaut.data.model.Slice +import io.micronaut.data.model.* import io.micronaut.data.model.query.builder.sql.Dialect import io.micronaut.data.repository.CrudRepository import jakarta.transaction.Transactional @@ -50,6 +48,14 @@ interface BookRepository : CrudRepository { // <2> fun list(pageable: Pageable): Slice // end::pageable[] + // tag::cursored-pageable[] + fun find(pageable: CursoredPageable): CursoredPage // <1> + + fun findByPagesBetween(minPageCount: Int, maxPageCount: Int, pageable: Pageable): CursoredPage // <2> + + fun findByTitleStartingWith(title: String, pageable: Pageable): Page // <3> + // end::cursored-pageable[] + // tag::simple-projection[] fun findTitleByPagesGreaterThan(pageCount: Int): List // end::simple-projection[] diff --git a/doc-examples/jdbc-example-kotlin/src/test/kotlin/example/BookRepositorySpec.kt b/doc-examples/jdbc-example-kotlin/src/test/kotlin/example/BookRepositorySpec.kt index 5c66854cda2..ccc966bb4e4 100644 --- a/doc-examples/jdbc-example-kotlin/src/test/kotlin/example/BookRepositorySpec.kt +++ b/doc-examples/jdbc-example-kotlin/src/test/kotlin/example/BookRepositorySpec.kt @@ -2,14 +2,15 @@ package example import io.micronaut.context.BeanContext import io.micronaut.data.annotation.Query +import io.micronaut.data.model.CursoredPageable import io.micronaut.data.model.Pageable +import io.micronaut.data.model.Sort import io.micronaut.test.extensions.junit5.annotation.MicronautTest +import jakarta.inject.Inject import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import java.util.* -import jakarta.inject.Inject -import org.junit.jupiter.api.BeforeAll -import org.junit.jupiter.api.BeforeEach @MicronautTest class BookRepositorySpec { @@ -129,6 +130,49 @@ class BookRepositorySpec { assertEquals(1, results.size) } + @Test + fun testCursoredPageable() { + bookRepository.saveAll( + Arrays.asList( + Book(0, "The Stand", 1000), + Book(0, "The Shining", 600), + Book(0, "The Power of the Dog", 500), + Book(0, "The Border", 700), + Book(0, "Along Came a Spider", 300), + Book(0, "Pet Cemetery", 400), + Book(0, "A Game of Thrones", 900), + Book(0, "A Clash of Kings", 1100) + ) + ) + + // tag::cursored-pageable[] + val page = // <1> + bookRepository.find(CursoredPageable.from(5, Sort.of(Sort.Order.asc("title")))) + val page2 = bookRepository.find(page.nextPageable()) // <2> + val pageByPagesBetween = // <3> + bookRepository.findByPagesBetween(400, 700, Pageable.from(0, 3)) + val pageByTitleStarts = // <4> + bookRepository.findByTitleStartingWith("The", CursoredPageable.from(3, Sort.unsorted())) + // end::cursored-pageable[] + + assertEquals( + 5, + page.numberOfElements + ) + assertEquals( + 3, + page2.numberOfElements + ) + assertEquals( + 3, + pageByPagesBetween.numberOfElements + ) + assertEquals( + 3, + pageByTitleStarts.numberOfElements + ) + } + @Test fun testDto() { bookRepository.save(Book(0, "The Shining", 400)) diff --git a/src/main/docs/guide/shared/querying/cursored-pagination.adoc b/src/main/docs/guide/shared/querying/cursored-pagination.adoc new file mode 100644 index 00000000000..fa370ac7ba9 --- /dev/null +++ b/src/main/docs/guide/shared/querying/cursored-pagination.adoc @@ -0,0 +1,22 @@ +Micronaut Data includes the ability to specify cursored pagination with the api:data.model.CursoredPageable[] type. +For cursored page methods return a api:data.model.CursoredPage[] type (inspired by https://jakarta.ee/specifications/data/1.0/apidocs/jakarta.data/jakarta/data/page/cursoredpage[CursoredPage] in Jakarta Data). + +WARNING: Cursored pagination is currently only supported with Micronaut Data JDBC and R2DBC. + +The following are some example signatures: + +snippet::example.BookRepository[project-base="doc-examples/jdbc-example", source="main", tags="cursored-pageable", indent="0"] + +<1> The signature defines a api:data.model.CursoredPageable[] parameter and api:data.model.CursoredPage[] return type. +<2> The signature of method defines a api:data.model.CursoredPage[] return type, therefore method will throw an error if the request is not for the first page or is not cursored. +<3> The method will return a api:data.model.CursoredPage[] only whenever a api:data.model.CursoredPageable[] is supplied. + +Therefore, you can use the repository methods to retrieve data with cursored pagination using the following queries: + +snippet::example.BookRepositorySpec[project-base="doc-examples/jdbc-example", tags="cursored-pageable", indent="0"] +<1> Create a cursored pageable with a desired size and sorting and get a cursored page. +<2> Get the next cursored pageable by calling `CursoredPage.getNextPageable()`. +<3> Request first cursored page. +<4> Supply a `CursoredPageable` to the repository method and a `CursoredPage` will be returned. + +NOTE: The cursor of pagination is based on the supplied sorting. If the supplied api:data.model.Sort[] in pageable does not produce a unique sorting, Micronaut Data internally will additionally sort by the identity column and extend the cursor with the column value to make sure pagination works correctly. diff --git a/src/main/docs/guide/toc.yml b/src/main/docs/guide/toc.yml index 6108eccca6d..488a9edf53e 100644 --- a/src/main/docs/guide/toc.yml +++ b/src/main/docs/guide/toc.yml @@ -15,6 +15,7 @@ shared: title: Writing Queries criteria: Query Criteria pagination: Pagination + cursored-pagination: Cursored Pagination ordering: Ordering projections: Query Projections dto: DTO Projections From 0d38e7d6c2975e28e04f170d44388660f4b1ce88 Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Wed, 24 Apr 2024 14:17:04 -0400 Subject: [PATCH 20/20] Fix interceptors --- .../DefaultAbstractFindPageInterceptor.java | 96 +++++++++++++++++++ .../DefaultFindCursoredPageInterceptor.java | 2 +- .../intercept/DefaultFindPageInterceptor.java | 58 +---------- 3 files changed, 98 insertions(+), 58 deletions(-) create mode 100644 data-runtime/src/main/java/io/micronaut/data/runtime/intercept/DefaultAbstractFindPageInterceptor.java diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/DefaultAbstractFindPageInterceptor.java b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/DefaultAbstractFindPageInterceptor.java new file mode 100644 index 00000000000..5001a0aca40 --- /dev/null +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/DefaultAbstractFindPageInterceptor.java @@ -0,0 +1,96 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.data.runtime.intercept; + +import io.micronaut.aop.MethodInvocationContext; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.util.CollectionUtils; +import io.micronaut.data.annotation.Query; +import io.micronaut.data.intercept.RepositoryMethodKey; +import io.micronaut.data.model.CursoredPage; +import io.micronaut.data.model.Page; +import io.micronaut.data.model.Pageable; +import io.micronaut.data.model.Pageable.Cursor; +import io.micronaut.data.model.Pageable.Mode; +import io.micronaut.data.model.runtime.PreparedQuery; +import io.micronaut.data.operations.RepositoryOperations; +import io.micronaut.data.runtime.operations.internal.sql.DefaultSqlPreparedQuery; + +import java.util.List; + +/** + * An abstract base implementation of query interceptor for page interceptors + * implementing {@link io.micronaut.data.intercept.FindPageInterceptor} or + * {@link io.micronaut.data.intercept.FindCursoredPageInterceptor}. + * + * @param The declaring type + * @param The paged type. + * @author graemerocher + * @since 4.8.0 + */ +public abstract class DefaultAbstractFindPageInterceptor extends AbstractQueryInterceptor { + + /** + * Default constructor. + * @param datastore The operations + */ + protected DefaultAbstractFindPageInterceptor(@NonNull RepositoryOperations datastore) { + super(datastore); + } + + @Override + public R intercept(RepositoryMethodKey methodKey, MethodInvocationContext context) { + Class returnType = context.getReturnType().getType(); + if (context.hasAnnotation(Query.class)) { + PreparedQuery preparedQuery = prepareQuery(methodKey, context); + + Iterable iterable = operations.findAll(preparedQuery); + List results = (List) CollectionUtils.iterableToList(iterable); + Pageable pageable = getPageable(context); + Long totalCount = null; + if (pageable.requestTotal()) { + PreparedQuery countQuery = prepareCountQuery(methodKey, context); + Number n = operations.findOne(countQuery); + totalCount = n != null ? n.longValue() : null; + } + + Page page; + if (pageable.getMode() == Mode.OFFSET) { + page = Page.of(results, pageable, totalCount); + } else if (preparedQuery instanceof DefaultSqlPreparedQuery sqlPreparedQuery) { + List cursors = sqlPreparedQuery.createCursors((List) results, pageable); + page = CursoredPage.of(results, pageable, cursors, totalCount); + } else { + throw new UnsupportedOperationException("Only offset pageable mode is supported by this query implementation"); + } + if (returnType.isInstance(page)) { + return (R) page; + } else { + return operations.getConversionService().convert(page, returnType) + .orElseThrow(() -> new IllegalStateException("Unsupported page interface type " + returnType)); + } + } else { + + Page page = operations.findPage(getPagedQuery(context)); + if (returnType.isInstance(page)) { + return (R) page; + } else { + return operations.getConversionService().convert(page, returnType) + .orElseThrow(() -> new IllegalStateException("Unsupported page interface type " + returnType)); + } + } + } +} diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/DefaultFindCursoredPageInterceptor.java b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/DefaultFindCursoredPageInterceptor.java index 37ef89a3570..f0309204523 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/DefaultFindCursoredPageInterceptor.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/DefaultFindCursoredPageInterceptor.java @@ -31,7 +31,7 @@ * @author Andriy Dmytruk * @since 4.8.0 */ -public class DefaultFindCursoredPageInterceptor extends DefaultFindPageInterceptor implements FindCursoredPageInterceptor { +public class DefaultFindCursoredPageInterceptor extends DefaultAbstractFindPageInterceptor implements FindCursoredPageInterceptor { /** * Default constructor. diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/DefaultFindPageInterceptor.java b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/DefaultFindPageInterceptor.java index 40268322a51..c4201330f47 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/DefaultFindPageInterceptor.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/intercept/DefaultFindPageInterceptor.java @@ -15,22 +15,9 @@ */ package io.micronaut.data.runtime.intercept; -import io.micronaut.aop.MethodInvocationContext; import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.util.CollectionUtils; -import io.micronaut.data.annotation.Query; import io.micronaut.data.intercept.FindPageInterceptor; -import io.micronaut.data.intercept.RepositoryMethodKey; -import io.micronaut.data.model.CursoredPage; -import io.micronaut.data.model.Page; -import io.micronaut.data.model.Pageable; -import io.micronaut.data.model.Pageable.Cursor; -import io.micronaut.data.model.Pageable.Mode; -import io.micronaut.data.model.runtime.PreparedQuery; import io.micronaut.data.operations.RepositoryOperations; -import io.micronaut.data.runtime.operations.internal.sql.DefaultSqlPreparedQuery; - -import java.util.List; /** * Default implementation of {@link FindPageInterceptor}. @@ -40,7 +27,7 @@ * @author graemerocher * @since 1.0.0 */ -public class DefaultFindPageInterceptor extends AbstractQueryInterceptor implements FindPageInterceptor { +public class DefaultFindPageInterceptor extends DefaultAbstractFindPageInterceptor implements FindPageInterceptor { /** * Default constructor. @@ -49,47 +36,4 @@ public class DefaultFindPageInterceptor extends AbstractQueryInterceptor context) { - Class returnType = context.getReturnType().getType(); - if (context.hasAnnotation(Query.class)) { - PreparedQuery preparedQuery = prepareQuery(methodKey, context); - - Iterable iterable = operations.findAll(preparedQuery); - List results = (List) CollectionUtils.iterableToList(iterable); - Pageable pageable = getPageable(context); - Long totalCount = null; - if (pageable.requestTotal()) { - PreparedQuery countQuery = prepareCountQuery(methodKey, context); - Number n = operations.findOne(countQuery); - totalCount = n != null ? n.longValue() : null; - } - - Page page; - if (pageable.getMode() == Mode.OFFSET) { - page = Page.of(results, pageable, totalCount); - } else if (preparedQuery instanceof DefaultSqlPreparedQuery sqlPreparedQuery) { - List cursors = sqlPreparedQuery.createCursors((List) results, pageable); - page = CursoredPage.of(results, pageable, cursors, totalCount); - } else { - throw new UnsupportedOperationException("Only offset pageable mode is supported by this query implementation"); - } - if (returnType.isInstance(page)) { - return (R) page; - } else { - return operations.getConversionService().convert(page, returnType) - .orElseThrow(() -> new IllegalStateException("Unsupported page interface type " + returnType)); - } - } else { - - Page page = operations.findPage(getPagedQuery(context)); - if (returnType.isInstance(page)) { - return (R) page; - } else { - return operations.getConversionService().convert(page, returnType) - .orElseThrow(() -> new IllegalStateException("Unsupported page interface type " + returnType)); - } - } - } }