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
*
* @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, R> 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, Number> 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, Number> 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, Number> 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));
- }
- }
- }
}