diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveFluentQueryByPredicate.java b/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveFluentQueryByPredicate.java new file mode 100644 index 000000000..dfa03d368 --- /dev/null +++ b/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveFluentQueryByPredicate.java @@ -0,0 +1,174 @@ +/* + * Copyright 2011-2021 the original author or 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 org.springframework.data.neo4j.repository.query; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.Collection; +import java.util.function.Function; + +import org.apiguardian.api.API; +import org.neo4j.cypherdsl.core.Cypher; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.neo4j.core.ReactiveFluentFindOperation; +import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity; +import org.springframework.data.repository.query.FluentQuery.ReactiveFluentQuery; +import org.springframework.data.support.PageableExecutionUtils; +import org.springframework.lang.Nullable; + +import com.querydsl.core.types.Predicate; + +/** + * Immutable implementation of a {@link ReactiveFluentQuery}. All + * methods that return a {@link ReactiveFluentQuery} return a new instance, the original instance won't be + * modified. + * + * @author Michael J. Simons + * @param Source type + * @param Result type + * @since 6.2 + */ +@API(status = API.Status.INTERNAL, since = "6.2") final class ReactiveFluentQueryByPredicate + extends FluentQuerySupport implements ReactiveFluentQuery { + + private final Predicate predicate; + + private final Neo4jPersistentEntity metaData; + + private final ReactiveFluentFindOperation findOperation; + + private final Function> countOperation; + + private final Function> existsOperation; + + ReactiveFluentQueryByPredicate( + Predicate predicate, + Neo4jPersistentEntity metaData, + Class resultType, + ReactiveFluentFindOperation findOperation, + Function> countOperation, + Function> existsOperation + ) { + this(predicate, metaData, resultType, findOperation, countOperation, existsOperation, Sort.unsorted(), null); + } + + ReactiveFluentQueryByPredicate( + Predicate predicate, + Neo4jPersistentEntity metaData, + Class resultType, + ReactiveFluentFindOperation findOperation, + Function> countOperation, + Function> existsOperation, + Sort sort, + @Nullable Collection properties + ) { + super(resultType, sort, properties); + this.predicate = predicate; + this.metaData = metaData; + this.findOperation = findOperation; + this.countOperation = countOperation; + this.existsOperation = existsOperation; + } + + @Override + @SuppressWarnings("HiddenField") + public ReactiveFluentQuery sortBy(Sort sort) { + + return new ReactiveFluentQueryByPredicate<>(this.predicate, this.metaData, this.resultType, this.findOperation, + this.countOperation, this.existsOperation, this.sort.and(sort), this.properties); + } + + @Override + @SuppressWarnings("HiddenField") + public ReactiveFluentQuery as(Class resultType) { + + return new ReactiveFluentQueryByPredicate<>(this.predicate, this.metaData, resultType, this.findOperation, + this.countOperation, this.existsOperation); + } + + @Override + @SuppressWarnings("HiddenField") + public ReactiveFluentQuery project(Collection properties) { + + return new ReactiveFluentQueryByPredicate<>(this.predicate, this.metaData, resultType, this.findOperation, + this.countOperation, this.existsOperation, sort, mergeProperties(properties)); + } + + @Override + public Mono one() { + + return findOperation.find(metaData.getType()) + .as(resultType) + .matching( + QueryFragmentsAndParameters.forCondition(metaData, + Cypher.adapt(predicate).asCondition(), + null, + CypherAdapterUtils.toSortItems(this.metaData, sort), + createIncludedFieldsPredicate())) + .one(); + } + + @Override + public Mono first() { + + return all().take(1).singleOrEmpty(); + } + + @Override + public Flux all() { + + return findOperation.find(metaData.getType()) + .as(resultType) + .matching( + QueryFragmentsAndParameters.forCondition(metaData, + Cypher.adapt(predicate).asCondition(), + null, + CypherAdapterUtils.toSortItems(this.metaData, sort), + createIncludedFieldsPredicate())) + .all(); + } + + @Override + public Mono> page(Pageable pageable) { + + Flux results = findOperation.find(metaData.getType()) + .as(resultType) + .matching( + QueryFragmentsAndParameters.forCondition(metaData, + Cypher.adapt(predicate).asCondition(), + pageable, null, + createIncludedFieldsPredicate())) + .all(); + + return results.collectList().zipWith(countOperation.apply(predicate)).map(tuple -> { + Page page = PageableExecutionUtils.getPage(tuple.getT1(), pageable, () -> tuple.getT2()); + return page; + }); + } + + @Override + public Mono count() { + return countOperation.apply(predicate); + } + + @Override + public Mono exists() { + return existsOperation.apply(predicate); + } +} diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveQuerydslNeo4jPredicateExecutor.java b/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveQuerydslNeo4jPredicateExecutor.java new file mode 100644 index 000000000..aa1fbef24 --- /dev/null +++ b/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveQuerydslNeo4jPredicateExecutor.java @@ -0,0 +1,142 @@ +/* + * Copyright 2011-2021 the original author or 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 org.springframework.data.neo4j.repository.query; + +import static org.neo4j.cypherdsl.core.Cypher.asterisk; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.Arrays; +import java.util.Collection; +import java.util.function.Function; + +import org.apiguardian.api.API; +import org.neo4j.cypherdsl.core.Condition; +import org.neo4j.cypherdsl.core.Conditions; +import org.neo4j.cypherdsl.core.Cypher; +import org.neo4j.cypherdsl.core.Functions; +import org.neo4j.cypherdsl.core.SortItem; +import org.neo4j.cypherdsl.core.Statement; +import org.reactivestreams.Publisher; +import org.springframework.data.domain.Sort; +import org.springframework.data.neo4j.core.ReactiveFluentFindOperation; +import org.springframework.data.neo4j.core.ReactiveNeo4jOperations; +import org.springframework.data.neo4j.core.mapping.CypherGenerator; +import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity; +import org.springframework.data.neo4j.repository.support.Neo4jEntityInformation; +import org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor; +import org.springframework.data.repository.query.FluentQuery.ReactiveFluentQuery; + +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.Predicate; + +/** + * Querydsl specific fragment for extending {@link org.springframework.data.neo4j.repository.support.SimpleReactiveNeo4jRepository} + * with an implementation of {@link ReactiveQuerydslPredicateExecutor}. Provides the necessary infrastructure for translating + * Query-DSL predicates into conditions that are passed along to the Cypher-DSL and eventually to the template infrastructure. + * This fragment will be loaded by the repository infrastructure when a repository is declared extending the above interface. + * + * @author Michael J. Simons + * @param The returned domain type. + * @since 6.2 + */ +@API(status = API.Status.INTERNAL, since = "6.2") +public final class ReactiveQuerydslNeo4jPredicateExecutor implements ReactiveQuerydslPredicateExecutor { + + private final Neo4jEntityInformation entityInformation; + + private final ReactiveNeo4jOperations neo4jOperations; + + private final Neo4jPersistentEntity metaData; + + public ReactiveQuerydslNeo4jPredicateExecutor(Neo4jEntityInformation entityInformation, + ReactiveNeo4jOperations neo4jOperations) { + + this.entityInformation = entityInformation; + this.neo4jOperations = neo4jOperations; + this.metaData = this.entityInformation.getEntityMetaData(); + } + + @Override + public Mono findOne(Predicate predicate) { + + return this.neo4jOperations.toExecutableQuery( + this.metaData.getType(), + QueryFragmentsAndParameters.forCondition(this.metaData, Cypher.adapt(predicate).asCondition(), null, + null) + ).flatMap(ReactiveNeo4jOperations.ExecutableQuery::getSingleResult); + } + + @Override + public Flux findAll(Predicate predicate) { + + return doFindAll(Cypher.adapt(predicate).asCondition(), null); + } + + @Override + public Flux findAll(Predicate predicate, Sort sort) { + + return doFindAll(Cypher.adapt(predicate).asCondition(), CypherAdapterUtils.toSortItems(this.metaData, sort)); + } + + @Override + public Flux findAll(Predicate predicate, OrderSpecifier... orders) { + return doFindAll(Cypher.adapt(predicate).asCondition(), Arrays.asList(QuerydslNeo4jPredicateExecutor.toSortItems(orders))); + + } + + @Override + public Flux findAll(OrderSpecifier... orders) { + + return doFindAll(Conditions.noCondition(), Arrays.asList(QuerydslNeo4jPredicateExecutor.toSortItems(orders))); + } + + private Flux doFindAll(Condition condition, Collection sortItems) { + return this.neo4jOperations.toExecutableQuery( + this.metaData.getType(), + QueryFragmentsAndParameters.forCondition(this.metaData, condition, null, + sortItems) + ).flatMapMany(ReactiveNeo4jOperations.ExecutableQuery::getResults); + } + + @Override + public Mono count(Predicate predicate) { + + Statement statement = CypherGenerator.INSTANCE.prepareMatchOf(this.metaData, + Cypher.adapt(predicate).asCondition()) + .returning(Functions.count(asterisk())).build(); + return this.neo4jOperations.count(statement, statement.getParameters()); + } + + @Override + public Mono exists(Predicate predicate) { + return findAll(predicate).hasElements(); + } + + @Override + public > P findBy(Predicate predicate, Function, P> queryFunction) { + + if (this.neo4jOperations instanceof ReactiveFluentFindOperation) { + @SuppressWarnings("unchecked") // defaultResultType will be a supertype of S and at this stage, the same. + ReactiveFluentQuery fluentQuery = (ReactiveFluentQuery) new ReactiveFluentQueryByPredicate<>(predicate, metaData, metaData.getType(), + (ReactiveFluentFindOperation) this.neo4jOperations, this::count, this::exists); + return queryFunction.apply(fluentQuery); + } + throw new UnsupportedOperationException( + "Fluent find by example not supported with standard Neo4jOperations. Must support fluent queries too."); + } +} diff --git a/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java b/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java index 0cb9ad38a..2e284e633 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java +++ b/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java @@ -17,7 +17,6 @@ package org.springframework.data.neo4j.repository.support; import java.util.Optional; -import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.neo4j.core.Neo4jOperations; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity; @@ -102,11 +101,6 @@ final class Neo4jRepositoryFactory extends RepositoryFactorySupport { private RepositoryFragment createDSLExecutorFragment(RepositoryMetadata metadata, Class implementor) { - if (metadata.isReactiveRepository()) { - throw new InvalidDataAccessApiUsageException( - "Cannot combine DSL executor and reactive repository support in a single interface"); - } - Neo4jEntityInformation entityInformation = getEntityInformation(metadata.getDomainType()); Object querydslFragment = instantiateClass(implementor, entityInformation, neo4jOperations); diff --git a/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactory.java b/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactory.java index 4cf97a222..b5f286ab2 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactory.java +++ b/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactory.java @@ -25,7 +25,10 @@ import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity; import org.springframework.data.neo4j.repository.ReactiveNeo4jRepository; import org.springframework.data.neo4j.repository.query.ReactiveNeo4jQueryLookupStrategy; +import org.springframework.data.neo4j.repository.query.ReactiveQuerydslNeo4jPredicateExecutor; import org.springframework.data.neo4j.repository.query.SimpleReactiveQueryByExampleExecutor; +import org.springframework.data.querydsl.QuerydslUtils; +import org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.ReactiveRepositoryFactorySupport; @@ -80,9 +83,25 @@ final class ReactiveNeo4jRepositoryFactory extends ReactiveRepositoryFactorySupp fragments = fragments.append(RepositoryFragment.implemented(byExampleExecutor)); + boolean isQueryDslRepository = QuerydslUtils.QUERY_DSL_PRESENT + && ReactiveQuerydslPredicateExecutor.class.isAssignableFrom(metadata.getRepositoryInterface()); + + if (isQueryDslRepository) { + + fragments = fragments.append(createDSLExecutorFragment(metadata, ReactiveQuerydslNeo4jPredicateExecutor.class)); + } + return fragments; } + private RepositoryFragment createDSLExecutorFragment(RepositoryMetadata metadata, Class implementor) { + + Neo4jEntityInformation entityInformation = getEntityInformation(metadata.getDomainType()); + Object querydslFragment = instantiateClass(implementor, entityInformation, neo4jOperations); + + return RepositoryFragment.implemented(querydslFragment); + } + @Override protected Class getRepositoryBaseClass(RepositoryMetadata metadata) { return SimpleReactiveNeo4jRepository.class; diff --git a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveQuerydslNeo4jPredicateExecutorIT.java b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveQuerydslNeo4jPredicateExecutorIT.java new file mode 100644 index 000000000..7a6594d14 --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveQuerydslNeo4jPredicateExecutorIT.java @@ -0,0 +1,309 @@ +/* + * Copyright 2011-2021 the original author or 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 org.springframework.data.neo4j.integration.reactive; + +import static org.assertj.core.api.Assertions.assertThat; + +import reactor.test.StepVerifier; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.neo4j.driver.Driver; +import org.neo4j.driver.Session; +import org.neo4j.driver.Transaction; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.data.neo4j.config.AbstractReactiveNeo4jConfig; +import org.springframework.data.neo4j.core.ReactiveDatabaseSelectionProvider; +import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager; +import org.springframework.data.neo4j.core.transaction.ReactiveNeo4jTransactionManager; +import org.springframework.data.neo4j.integration.shared.common.Person; +import org.springframework.data.neo4j.repository.ReactiveNeo4jRepository; +import org.springframework.data.neo4j.repository.config.EnableReactiveNeo4jRepositories; +import org.springframework.data.neo4j.test.BookmarkCapture; +import org.springframework.data.neo4j.test.Neo4jExtension; +import org.springframework.data.neo4j.test.Neo4jIntegrationTest; +import org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor; +import org.springframework.transaction.ReactiveTransactionManager; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +import com.querydsl.core.types.Ops; +import com.querydsl.core.types.Order; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.Predicate; +import com.querydsl.core.types.dsl.Expressions; + +/** + * @author Michael J. Simons + */ +@Neo4jIntegrationTest +class ReactiveQuerydslNeo4jPredicateExecutorIT { + + protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; + + private final Path personPath; + private final Path firstNamePath; + private final Path lastNamePath; + + ReactiveQuerydslNeo4jPredicateExecutorIT() { + this.personPath = Expressions.path(Person.class, "person"); + this.firstNamePath = Expressions.path(String.class, personPath, "firstName"); + this.lastNamePath = Expressions.path(String.class, personPath, "lastName"); + } + + @BeforeAll + protected static void setupData(@Autowired BookmarkCapture bookmarkCapture) { + + try (Session session = neo4jConnectionSupport.getDriver().session(bookmarkCapture.createSessionConfig()); + Transaction transaction = session.beginTransaction() + ) { + transaction.run("MATCH (n) detach delete n"); + transaction.run("CREATE (p:Person{firstName: 'A', lastName: 'LA'})"); + transaction.run("CREATE (p:Person{firstName: 'B', lastName: 'LB'})"); + transaction + .run("CREATE (p:Person{firstName: 'Helge', lastName: 'Schneider'}) -[:LIVES_AT]-> (a:Address {city: 'Mülheim an der Ruhr'})"); + transaction.run("CREATE (p:Person{firstName: 'Bela', lastName: 'B.'})"); + transaction.commit(); + bookmarkCapture.seedWith(session.lastBookmark()); + } + } + + @Test // GH-2361 + void fluentFindOneShouldWork(@Autowired QueryDSLPersonRepository repository) { + + Predicate predicate = Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")); + repository.findBy(predicate, q -> q.one()) + .map(Person::getLastName) + .as(StepVerifier::create) + .expectNext("Schneider") + .verifyComplete(); + } + + @Test // GH-2361 + void fluentFindAllShouldWork(@Autowired QueryDSLPersonRepository repository) { + + Predicate predicate = Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")) + .or(Expressions.predicate(Ops.EQ, lastNamePath, Expressions.asString("B."))); + repository.findBy(predicate, q -> q.all()) + .map(Person::getFirstName) + .sort() // Due to not having something like containsExactlyInAnyOrder + .as(StepVerifier::create) + .expectNext("Bela", "Helge") + .verifyComplete(); + } + + @Test // GH-2361 + void fluentFindAllProjectingShouldWork(@Autowired QueryDSLPersonRepository repository) { + + Predicate predicate = Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")); + repository.findBy(predicate, q -> q.project("firstName").all()) + .as(StepVerifier::create) + .expectNextMatches(p -> { + assertThat(p.getFirstName()).isEqualTo("Helge"); + assertThat(p.getId()).isNotNull(); + + assertThat(p.getLastName()).isNull(); + assertThat(p.getAddress()).isNull(); + return true; + }) + .verifyComplete(); + } + + static class DtoPersonProjection { + + private final String firstName; + + DtoPersonProjection(String firstName) { + this.firstName = firstName; + } + + public String getFirstName() { + return firstName; + } + } + + @Test // GH-2361 + void fluentfindAllAsShouldWork(@Autowired QueryDSLPersonRepository repository) { + + Predicate predicate = Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")); + repository.findBy(predicate, q -> q.as(DtoPersonProjection.class).all()) + .map(DtoPersonProjection::getFirstName) + .as(StepVerifier::create) + .expectNext("Helge") + .verifyComplete(); + } + + @Test // GH-2361 + void fluentFindFirstShouldWork(@Autowired QueryDSLPersonRepository repository) { + + Predicate predicate = Expressions.TRUE.isTrue(); + repository.findBy(predicate, q -> q.sortBy(Sort.by(Sort.Direction.DESC, "lastName")).first()) + .map(Person::getFirstName) + .as(StepVerifier::create) + .expectNext("Helge") + .verifyComplete(); + } + + @Test // GH-2361 + void fluentFindAllWithSortShouldWork(@Autowired QueryDSLPersonRepository repository) { + + Predicate predicate = Expressions.TRUE.isTrue(); + repository.findBy(predicate, q -> q.sortBy(Sort.by(Sort.Direction.DESC, "lastName")).all()) + .map(Person::getLastName) + .as(StepVerifier::create) + .expectNext("Schneider", "LB", "LA", "B.") + .verifyComplete(); + } + + @Test // GH-2361 + void fluentFindAllWithPaginationShouldWork(@Autowired QueryDSLPersonRepository repository) { + + Predicate predicate = Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")) + .or(Expressions.predicate(Ops.EQ, lastNamePath, Expressions.asString("B."))); + repository.findBy(predicate, q -> q.page(PageRequest.of(1, 1, Sort.by("lastName").ascending()))) + .as(StepVerifier::create) + .expectNextMatches(people -> { + + assertThat(people).extracting(Person::getFirstName).containsExactly("Helge"); + assertThat(people.hasPrevious()).isTrue(); + assertThat(people.hasNext()).isFalse(); + return true; + }).verifyComplete(); + } + + @Test // GH-2361 + void fluentExistsShouldWork(@Autowired QueryDSLPersonRepository repository) { + + Predicate predicate = Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")); + repository.findBy(predicate, q -> q.exists()).as(StepVerifier::create).expectNext(true).verifyComplete(); + } + + @Test // GH-2361 + void fluentCountShouldWork(@Autowired QueryDSLPersonRepository repository) { + + Predicate predicate = Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")) + .or(Expressions.predicate(Ops.EQ, lastNamePath, Expressions.asString("B."))); + repository.findBy(predicate, q -> q.count()).as(StepVerifier::create).expectNext(2L).verifyComplete(); + } + + @Test // GH-2361 + void findOneShouldWork(@Autowired QueryDSLPersonRepository repository) { + + repository.findOne(Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge"))) + .map(Person::getLastName) + .as(StepVerifier::create) + .expectNext("Schneider") + .verifyComplete(); + } + + @Test // GH-2361 + void findAllShouldWork(@Autowired QueryDSLPersonRepository repository) { + + repository.findAll(Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")) + .or(Expressions.predicate(Ops.EQ, lastNamePath, Expressions.asString("B.")))) + .map(Person::getFirstName) + .sort() // Due to not having something like containsExactlyInAnyOrder + .as(StepVerifier::create) + .expectNext("Bela", "Helge") + .verifyComplete(); + } + + @Test // GH-2361 + void sortedFindAllShouldWork(@Autowired QueryDSLPersonRepository repository) { + + repository.findAll(Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")) + .or(Expressions.predicate(Ops.EQ, lastNamePath, Expressions.asString("B."))), + new OrderSpecifier(Order.DESC, lastNamePath) + ) + .map(Person::getFirstName) + .as(StepVerifier::create) + .expectNext("Helge", "Bela") + .verifyComplete(); + } + + @Test // GH-2361 + void orderedFindAllShouldWork(@Autowired QueryDSLPersonRepository repository) { + + repository.findAll(Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")) + .or(Expressions.predicate(Ops.EQ, lastNamePath, Expressions.asString("B."))), + Sort.by("lastName").descending() + ) + .map(Person::getFirstName) + .as(StepVerifier::create) + .expectNext("Helge", "Bela") + .verifyComplete(); + } + + @Test // GH-2361 + void orderedFindAllWithoutPredicateShouldWork(@Autowired QueryDSLPersonRepository repository) { + + repository.findAll(new OrderSpecifier(Order.DESC, lastNamePath)) + .map(Person::getFirstName) + .as(StepVerifier::create) + .expectNext("Helge", "B", "A", "Bela") + .verifyComplete(); + } + + @Test // GH-2361 + void countShouldWork(@Autowired QueryDSLPersonRepository repository) { + + repository.count(Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")) + .or(Expressions.predicate(Ops.EQ, lastNamePath, Expressions.asString("B."))) + ).as(StepVerifier::create) + .expectNext(2L) + .verifyComplete(); + } + + @Test // GH-2361 + void existsShouldWork(@Autowired QueryDSLPersonRepository repository) { + + repository.exists(Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("A"))) + .as(StepVerifier::create) + .expectNext(true) + .verifyComplete(); + } + + interface QueryDSLPersonRepository extends ReactiveNeo4jRepository, ReactiveQuerydslPredicateExecutor { + } + + @Configuration + @EnableTransactionManagement + @EnableReactiveNeo4jRepositories(considerNestedRepositories = true) + static class Config extends AbstractReactiveNeo4jConfig { + + @Bean + public Driver driver() { + + return neo4jConnectionSupport.getDriver(); + } + + @Bean + public BookmarkCapture bookmarkCapture() { + return new BookmarkCapture(); + } + + @Override + public ReactiveTransactionManager reactiveTransactionManager(Driver driver, ReactiveDatabaseSelectionProvider databaseNameProvider) { + + BookmarkCapture bookmarkCapture = bookmarkCapture(); + return new ReactiveNeo4jTransactionManager(driver, databaseNameProvider, Neo4jBookmarkManager.create(bookmarkCapture)); + } + } +}