Skip to content

Commit

Permalink
Initial addition of cursored pagination for SQL (#2884)
Browse files Browse the repository at this point in the history
* 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.

* Checkstyle

* Change base to 4.8.x

* Add hasNext and hasPrevious methods to Page

* Checkstyle fixes and disable PostgreSQL test

* Implement some review comments

* Add methods corresponding to the jakarta PageRequest API

* Add requestTotal property to the pageable

* Update Page to account for cases when total size is not queried

* Fix build

* Implement more review comments

* Add tests

* Fix checkstyle

* Throw UnsupportedOperationException where cursored pageable is not yet supported

* Add all cursors to the page implementation and remove nextPageable and previousPageable implementation from the CursoredPageable

* Slightly improve the test

* Fix for postgres r2dbc test

* Add CursoredPage and interceptor

* Add documentation for cursored pageable

* Fix interceptors
  • Loading branch information
andriy-dmytruk authored May 22, 2024
1 parent 477eabc commit 696cc25
Show file tree
Hide file tree
Showing 63 changed files with 2,495 additions and 180 deletions.
2 changes: 2 additions & 0 deletions config/checkstyle/suppressions.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@
<!-- files="DefaultBeanContext.java|BeanDefinitionWriter.java|DefaultHttpClient.java"/> -->

<suppress checks="MissingJavadocType" files=".*doc-examples.*" />
<suppress checks="FileLength" files=".*AbstractSqlLikeQueryBuilder.*" />

</suppressions>
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -329,6 +330,9 @@ public Map<String, String> 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(" ");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,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;
Expand Down Expand Up @@ -336,6 +337,9 @@ protected <R> 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,
Expand Down Expand Up @@ -600,6 +604,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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,14 +84,18 @@ protected Publisher<?> interceptPublisher(RepositoryMethodKey methodKey, MethodI
return operations.withSession(session -> {
if (pageable.isUnpaged()) {
return Mono.fromCompletionStage(() -> session.createQuery(query).getResultList())
.map(resultList -> Page.of(resultList, pageable, resultList.size()));
.map(resultList -> Page.of(resultList, pageable, (long) resultList.size()));
}
return Mono.fromCompletionStage(() -> {
Stage.SelectionQuery<Object> q = session.createQuery(query);
q.setFirstResult((int) pageable.getOffset());
q.setMaxResults(pageable.getSize());
return q.getResultList();
}).flatMap(results -> {
if (!pageable.requestTotal()) {
return Mono.just(Page.of(results, pageable, null));
}

final CriteriaQuery<Long> countQuery = criteriaBuilder.createQuery(Long.class);
final Root<Object> countRoot = countQuery.from(rootEntity);
final Predicate countPredicate = specification != null ? specification.toPredicate(countRoot, countQuery, criteriaBuilder) : null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -733,7 +733,7 @@ public <T> Stream<T> findStream(@NonNull PagedQuery<T> query) {

@Override
public <R> Page<R> findPage(@NonNull PagedQuery<R> query) {
throw new UnsupportedOperationException("The findPage method without an explicit query is not supported. Use findPage(PreparedQuery) instead");
throw new UnsupportedOperationException("The findPage method without an explicit query is not supported. Use findAll(PreparedQuery) instead");
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -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()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright 2017-2020 original authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.micronaut.data.jdbc.mysql

import groovy.transform.Memoized
import io.micronaut.context.ApplicationContext
import io.micronaut.data.tck.repositories.BookRepository
import io.micronaut.data.tck.repositories.PersonRepository
import io.micronaut.data.tck.tests.AbstractCursoredPageSpec
import spock.lang.AutoCleanup
import spock.lang.Shared

class MysqlCursoredPaginationSpec extends AbstractCursoredPageSpec implements MySQLTestPropertyProvider {

@Shared @AutoCleanup ApplicationContext context

@Memoized
@Override
PersonRepository getPersonRepository() {
return context.getBean(MySqlPersonRepository)
}

@Memoized
@Override
BookRepository getBookRepository() {
return context.getBean(MySqlBookRepository)
}

@Override
void init() {
context = ApplicationContext.run(properties)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright 2017-2020 original authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.micronaut.data.jdbc.oraclexe

import groovy.transform.Memoized
import io.micronaut.context.ApplicationContext
import io.micronaut.data.tck.repositories.BookRepository
import io.micronaut.data.tck.repositories.PersonRepository
import io.micronaut.data.tck.tests.AbstractCursoredPageSpec
import spock.lang.AutoCleanup
import spock.lang.Shared

class OracleXECursoredPaginationSpec extends AbstractCursoredPageSpec implements OracleTestPropertyProvider {

@Shared @AutoCleanup ApplicationContext context

@Override
@Memoized
PersonRepository getPersonRepository() {
return context.getBean(OracleXEPersonRepository)
}

@Override
@Memoized
BookRepository getBookRepository() {
return context.getBean(OracleXEBookRepository)
}

@Override
void init() {
context = ApplicationContext.run(properties)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright 2017-2020 original authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.micronaut.data.jdbc.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())
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package io.micronaut.data.jdbc.sqlserver

import groovy.transform.Memoized
import io.micronaut.context.ApplicationContext
import io.micronaut.data.tck.repositories.BookRepository
import io.micronaut.data.tck.repositories.PersonRepository
Expand All @@ -26,11 +27,13 @@ class SqlServerPaginationSpec extends AbstractPageSpec implements MSSQLTestPrope

@Shared @AutoCleanup ApplicationContext context

@Memoized
@Override
PersonRepository getPersonRepository() {
return context.getBean(MSSQLPersonRepository)
}

@Memoized
@Override
BookRepository getBookRepository() {
return context.getBean(MSBookRepository)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,29 +96,33 @@ public Page intercept(RepositoryMethodKey methodKey, MethodInvocationContext<Obj
return Page.of(
resultList,
pageable,
resultList.size()
(long) resultList.size()
);
} else {
typedQuery.setFirstResult((int) pageable.getOffset());
typedQuery.setMaxResults(pageable.getSize());
final List<Object> results = typedQuery.getResultList();
final CriteriaQuery<Long> countQuery = criteriaBuilder.createQuery(Long.class);
final Root<?> countRoot = countQuery.from(rootEntity);
final Predicate countPredicate = specification != null ? specification.toPredicate(countRoot, countQuery, criteriaBuilder) : null;
if (countPredicate != null) {
countQuery.where(countPredicate);
}
if (countQuery.isDistinct()) {
countQuery.select(criteriaBuilder.countDistinct(countRoot));
} else {
countQuery.select(criteriaBuilder.count(countRoot));

Long totalCount = null;
if (pageable.requestTotal()) {
final CriteriaQuery<Long> countQuery = criteriaBuilder.createQuery(Long.class);
final Root<?> countRoot = countQuery.from(rootEntity);
final Predicate countPredicate = specification != null ? specification.toPredicate(countRoot, countQuery, criteriaBuilder) : null;
if (countPredicate != null) {
countQuery.where(countPredicate);
}
if (countQuery.isDistinct()) {
countQuery.select(criteriaBuilder.countDistinct(countRoot));
} else {
countQuery.select(criteriaBuilder.count(countRoot));
}
totalCount = entityManager.createQuery(countQuery).getSingleResult();
}
Long singleResult = entityManager.createQuery(countQuery).getSingleResult();

return Page.of(
results,
pageable,
singleResult
totalCount
);
}

Expand Down
Loading

0 comments on commit 696cc25

Please sign in to comment.