From 2186367cb86621483be4ff59d777df2c542d1b97 Mon Sep 17 00:00:00 2001 From: Gerrit Meier Date: Mon, 22 May 2023 12:45:39 +0200 Subject: [PATCH] GH-2726 - Add scroll support to FluentQuery. This will implement limit() and scroll() methods in all (Reactive)FluentQuery.. classes. Closes #2726 --- .../neo4j/core/mapping/CypherGenerator.java | 2 +- .../mapping/NestedRelationshipContext.java | 2 +- .../neo4j/core/mapping/PropertyFilter.java | 3 + .../repository/query/CypherAdapterUtils.java | 5 +- .../repository/query/CypherQueryCreator.java | 2 +- .../query/CypherdslConditionExecutorImpl.java | 20 +- .../query/FetchableFluentQueryByExample.java | 72 ++++-- .../FetchableFluentQueryByPredicate.java | 67 ++++-- .../repository/query/FluentQuerySupport.java | 62 +++++- .../repository/query/Neo4jQuerySupport.java | 9 +- .../query/QueryFragmentsAndParameters.java | 206 +++++++++++------- .../query/QuerydslNeo4jPredicateExecutor.java | 13 +- ...eactiveCypherdslConditionExecutorImpl.java | 17 +- .../query/ReactiveFluentQueryByExample.java | 51 ++++- .../query/ReactiveFluentQueryByPredicate.java | 66 ++++-- ...eactiveQuerydslNeo4jPredicateExecutor.java | 16 +- .../query/SimpleQueryByExampleExecutor.java | 5 +- .../SimpleReactiveQueryByExampleExecutor.java | 3 +- .../support/Neo4jRepositoryFactory.java | 10 +- .../ReactiveNeo4jRepositoryFactory.java | 10 +- .../QuerydslNeo4jPredicateExecutorIT.java | 107 +++++++++ .../integration/imperative/RepositoryIT.java | 28 +++ .../integration/imperative/ScrollingIT.java | 65 ++++++ ...ctiveQuerydslNeo4jPredicateExecutorIT.java | 66 ++++++ .../reactive/ReactiveRepositoryIT.java | 25 +++ .../reactive/ReactiveScrollingIT.java | 93 ++++++++ .../shared/common/ScrollingEntity.java | 1 + .../query/CypherAdapterUtilsTest.java | 2 +- 28 files changed, 860 insertions(+), 168 deletions(-) diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/CypherGenerator.java b/src/main/java/org/springframework/data/neo4j/core/mapping/CypherGenerator.java index dc6519000..900111edc 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/CypherGenerator.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/CypherGenerator.java @@ -624,7 +624,7 @@ public enum CypherGenerator { } public Collection createReturnStatementForMatch(Neo4jPersistentEntity nodeDescription) { - return createReturnStatementForMatch(nodeDescription, (pp -> true)); + return createReturnStatementForMatch(nodeDescription, PropertyFilter.NO_FILTER); } /** diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/NestedRelationshipContext.java b/src/main/java/org/springframework/data/neo4j/core/mapping/NestedRelationshipContext.java index aa1f3b104..8b9acec74 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/NestedRelationshipContext.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/NestedRelationshipContext.java @@ -125,7 +125,7 @@ public final class NestedRelationshipContext { Object value = propertyAccessor.getProperty(inverse); boolean inverseValueIsEmpty = value == null; - RelationshipDescription relationship = neo4jPersistentEntity.getRelationshipsInHierarchy((pp -> true)).stream() + RelationshipDescription relationship = neo4jPersistentEntity.getRelationshipsInHierarchy((PropertyFilter.NO_FILTER)).stream() .filter(r -> r.getFieldName().equals(inverse.getName())).findFirst().orElseThrow(() -> new MappingException( neo4jPersistentEntity.getName() + " does not define a relationship for " + inverse.getFieldName())); diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/PropertyFilter.java b/src/main/java/org/springframework/data/neo4j/core/mapping/PropertyFilter.java index 7a1c32377..f30c63a87 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/PropertyFilter.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/PropertyFilter.java @@ -19,6 +19,7 @@ import java.util.Collection; import java.util.HashSet; import java.util.Optional; import java.util.Set; +import java.util.function.Predicate; import org.apiguardian.api.API; import org.springframework.data.neo4j.core.schema.Property; @@ -39,6 +40,8 @@ public abstract class PropertyFilter { return new NonFilteringPropertyFilter(); } + public static final Predicate NO_FILTER = (pp) -> true; + public abstract boolean contains(String dotPath, Class typeToCheck); public abstract boolean contains(RelaxedPropertyPath propertyPath); diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/CypherAdapterUtils.java b/src/main/java/org/springframework/data/neo4j/repository/query/CypherAdapterUtils.java index bdc0540cb..3be7f8968 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/CypherAdapterUtils.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/CypherAdapterUtils.java @@ -39,6 +39,7 @@ import org.springframework.data.domain.KeysetScrollPosition; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.ScrollPosition.Direction; import org.springframework.data.domain.Sort; +import org.springframework.data.neo4j.core.convert.Neo4jConversionService; import org.springframework.data.neo4j.core.mapping.Constants; import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity; import org.springframework.data.neo4j.core.mapping.Neo4jPersistentProperty; @@ -98,7 +99,7 @@ public final class CypherAdapterUtils { }; } - public static Condition combineKeysetIntoCondition(Neo4jPersistentEntity entity, KeysetScrollPosition scrollPosition, Sort sort) { + public static Condition combineKeysetIntoCondition(Neo4jPersistentEntity entity, KeysetScrollPosition scrollPosition, Sort sort, Neo4jConversionService conversionService) { var incomingKeys = scrollPosition.getKeys(); var orderedKeys = new LinkedHashMap(); @@ -135,7 +136,7 @@ public final class CypherAdapterUtils { if (v == null || (v instanceof Value value && value.isNull())) { throw new IllegalStateException("Cannot resume from KeysetScrollPosition. Offending key: '%s' is 'null'".formatted(k)); } - var parameter = Cypher.anonParameter(v); + var parameter = Cypher.anonParameter(conversionService.convert(v, Value.class)); Expression expression; diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java b/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java index c9593aa22..03a60ba1d 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java @@ -239,7 +239,7 @@ final class CypherQueryCreator extends AbstractQueryCreator implements CypherdslConditi return this.neo4jOperations.toExecutableQuery( this.metaData.getType(), - QueryFragmentsAndParameters.forCondition(this.metaData, condition, null, null) + QueryFragmentsAndParameters.forCondition(this.metaData, condition) ).getSingleResult(); } @@ -75,17 +77,18 @@ public final class CypherdslConditionExecutorImpl implements CypherdslConditi return this.neo4jOperations.toExecutableQuery( this.metaData.getType(), - QueryFragmentsAndParameters.forCondition(this.metaData, condition, null, null) + QueryFragmentsAndParameters.forCondition(this.metaData, condition) ).getResults(); } @Override public Collection findAll(Condition condition, Sort sort) { + Predicate noFilter = PropertyFilter.NO_FILTER; return this.neo4jOperations.toExecutableQuery( metaData.getType(), - QueryFragmentsAndParameters.forCondition( - this.metaData, condition, null, CypherAdapterUtils.toSortItems(this.metaData, sort) + QueryFragmentsAndParameters.forConditionAndSort( + this.metaData, condition, sort, null, noFilter ) ).getResults(); } @@ -95,8 +98,8 @@ public final class CypherdslConditionExecutorImpl implements CypherdslConditi return this.neo4jOperations.toExecutableQuery( this.metaData.getType(), - QueryFragmentsAndParameters.forCondition( - this.metaData, condition, null, Arrays.asList(sortItems) + QueryFragmentsAndParameters.forConditionAndSortItems( + this.metaData, condition, Arrays.asList(sortItems) ) ).getResults(); } @@ -106,16 +109,17 @@ public final class CypherdslConditionExecutorImpl implements CypherdslConditi return this.neo4jOperations.toExecutableQuery( this.metaData.getType(), - QueryFragmentsAndParameters.forCondition(this.metaData, Conditions.noCondition(), null, Arrays.asList(sortItems)) + QueryFragmentsAndParameters.forConditionAndSortItems(this.metaData, Conditions.noCondition(), Arrays.asList(sortItems)) ).getResults(); } @Override public Page findAll(Condition condition, Pageable pageable) { + Predicate noFilter = PropertyFilter.NO_FILTER; List page = this.neo4jOperations.toExecutableQuery( this.metaData.getType(), - QueryFragmentsAndParameters.forCondition(this.metaData, condition, pageable, null) + QueryFragmentsAndParameters.forConditionAndPageable(this.metaData, condition, pageable, noFilter) ).getResults(); LongSupplier totalCountSupplier = () -> this.count(condition); return PageableExecutionUtils.getPage(page, pageable, totalCountSupplier); diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/FetchableFluentQueryByExample.java b/src/main/java/org/springframework/data/neo4j/repository/query/FetchableFluentQueryByExample.java index eea08a560..ef6030e38 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/FetchableFluentQueryByExample.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/FetchableFluentQueryByExample.java @@ -15,23 +15,29 @@ */ package org.springframework.data.neo4j.repository.query; +import org.apiguardian.api.API; +import org.neo4j.cypherdsl.core.Condition; +import org.springframework.data.domain.Example; +import org.springframework.data.domain.KeysetScrollPosition; +import org.springframework.data.domain.OffsetScrollPosition; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.ScrollPosition; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Window; +import org.springframework.data.neo4j.core.FluentFindOperation; +import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; +import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity; +import org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery; +import org.springframework.data.support.PageableExecutionUtils; +import org.springframework.lang.Nullable; + import java.util.Collection; import java.util.List; import java.util.function.Function; import java.util.function.LongSupplier; import java.util.stream.Stream; -import org.apiguardian.api.API; -import org.springframework.data.domain.Example; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.data.neo4j.core.FluentFindOperation; -import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; -import org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery; -import org.springframework.data.support.PageableExecutionUtils; -import org.springframework.lang.Nullable; - /** * Immutable implementation of a {@link FetchableFluentQuery}. All * methods that return a {@link FetchableFluentQuery} return a new instance, the original instance won't be @@ -64,7 +70,7 @@ final class FetchableFluentQueryByExample extends FluentQuerySupport im Function, Boolean> existsOperation ) { this(example, resultType, mappingContext, findOperation, countOperation, existsOperation, Sort.unsorted(), - null); + null, null); } FetchableFluentQueryByExample( @@ -75,9 +81,10 @@ final class FetchableFluentQueryByExample extends FluentQuerySupport im Function, Long> countOperation, Function, Boolean> existsOperation, Sort sort, + @Nullable Integer limit, @Nullable Collection properties ) { - super(resultType, sort, properties); + super(resultType, sort, limit, properties); this.mappingContext = mappingContext; this.example = example; this.findOperation = findOperation; @@ -90,7 +97,14 @@ final class FetchableFluentQueryByExample extends FluentQuerySupport im public FetchableFluentQuery sortBy(Sort sort) { return new FetchableFluentQueryByExample<>(this.example, this.resultType, this.mappingContext, this.findOperation, - this.countOperation, this.existsOperation, this.sort.and(sort), this.properties); + this.countOperation, this.existsOperation, this.sort.and(sort), this.limit, this.properties); + } + + @Override + @SuppressWarnings("HiddenField") + public FetchableFluentQuery limit(int limit) { + return new FetchableFluentQueryByExample<>(this.example, this.resultType, this.mappingContext, this.findOperation, + this.countOperation, this.existsOperation, this.sort, limit, this.properties); } @Override @@ -106,7 +120,7 @@ final class FetchableFluentQueryByExample extends FluentQuerySupport im public FetchableFluentQuery project(Collection properties) { return new FetchableFluentQueryByExample<>(this.example, this.resultType, this.mappingContext, this.findOperation, - this.countOperation, this.existsOperation, this.sort, mergeProperties(properties)); + this.countOperation, this.existsOperation, this.sort, this.limit, mergeProperties(properties)); } @Override @@ -114,7 +128,7 @@ final class FetchableFluentQueryByExample extends FluentQuerySupport im return findOperation.find(example.getProbeType()) .as(resultType) - .matching(QueryFragmentsAndParameters.forExample(mappingContext, example, sort, + .matching(QueryFragmentsAndParameters.forExampleWithSort(mappingContext, example, sort, limit, createIncludedFieldsPredicate())) .oneValue(); } @@ -131,7 +145,7 @@ final class FetchableFluentQueryByExample extends FluentQuerySupport im return findOperation.find(example.getProbeType()) .as(resultType) - .matching(QueryFragmentsAndParameters.forExample(mappingContext, example, sort, + .matching(QueryFragmentsAndParameters.forExampleWithSort(mappingContext, example, sort, limit, createIncludedFieldsPredicate())) .all(); } @@ -141,7 +155,7 @@ final class FetchableFluentQueryByExample extends FluentQuerySupport im List page = findOperation.find(example.getProbeType()) .as(resultType) - .matching(QueryFragmentsAndParameters.forExample(mappingContext, example, pageable, + .matching(QueryFragmentsAndParameters.forExampleWithPageable(mappingContext, example, pageable, createIncludedFieldsPredicate())) .all(); @@ -149,6 +163,28 @@ final class FetchableFluentQueryByExample extends FluentQuerySupport im return PageableExecutionUtils.getPage(page, pageable, totalCountSupplier); } + @Override + public Window scroll(ScrollPosition scrollPosition) { + Class domainType = this.example.getProbeType(); + Neo4jPersistentEntity entity = mappingContext.getPersistentEntity(domainType); + + var skip = scrollPosition.isInitial() + ? 0 + : (scrollPosition instanceof OffsetScrollPosition offsetScrollPosition) ? offsetScrollPosition.getOffset() + : 0; + + Condition condition = scrollPosition instanceof KeysetScrollPosition keysetScrollPosition + ? CypherAdapterUtils.combineKeysetIntoCondition(mappingContext.getPersistentEntity(example.getProbeType()), keysetScrollPosition, sort, mappingContext.getConversionService()) + : null; + + List rawResult = findOperation.find(domainType) + .as(resultType) + .matching(QueryFragmentsAndParameters.forExampleWithScrollPosition(mappingContext, example, condition, sort, limit == null ? 1 : limit + 1, skip, scrollPosition, createIncludedFieldsPredicate())) + .all(); + + return scroll(scrollPosition, rawResult, entity); + } + @Override public Stream stream() { return all().stream(); diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/FetchableFluentQueryByPredicate.java b/src/main/java/org/springframework/data/neo4j/repository/query/FetchableFluentQueryByPredicate.java index 9d4210069..19159f6d7 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/FetchableFluentQueryByPredicate.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/FetchableFluentQueryByPredicate.java @@ -23,10 +23,14 @@ import java.util.stream.Stream; import org.apiguardian.api.API; import org.neo4j.cypherdsl.core.Cypher; +import org.springframework.data.domain.KeysetScrollPosition; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.ScrollPosition; import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Window; import org.springframework.data.neo4j.core.FluentFindOperation; +import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity; import org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery; import org.springframework.data.support.PageableExecutionUtils; @@ -58,29 +62,35 @@ final class FetchableFluentQueryByPredicate extends FluentQuerySupport private final Function existsOperation; + private final Neo4jMappingContext mappingContext; + FetchableFluentQueryByPredicate( Predicate predicate, + Neo4jMappingContext mappingContext, Neo4jPersistentEntity metaData, Class resultType, FluentFindOperation findOperation, Function countOperation, Function existsOperation ) { - this(predicate, metaData, resultType, findOperation, countOperation, existsOperation, Sort.unsorted(), null); + this(predicate, mappingContext, metaData, resultType, findOperation, countOperation, existsOperation, Sort.unsorted(), null, null); } FetchableFluentQueryByPredicate( Predicate predicate, + Neo4jMappingContext mappingContext, Neo4jPersistentEntity metaData, Class resultType, FluentFindOperation findOperation, Function countOperation, Function existsOperation, Sort sort, + @Nullable Integer limit, @Nullable Collection properties ) { - super(resultType, sort, properties); + super(resultType, sort, limit, properties); this.predicate = predicate; + this.mappingContext = mappingContext; this.metaData = metaData; this.findOperation = findOperation; this.countOperation = countOperation; @@ -91,15 +101,22 @@ final class FetchableFluentQueryByPredicate extends FluentQuerySupport @SuppressWarnings("HiddenField") public FetchableFluentQuery sortBy(Sort sort) { - return new FetchableFluentQueryByPredicate<>(this.predicate, this.metaData, this.resultType, this.findOperation, - this.countOperation, this.existsOperation, this.sort.and(sort), this.properties); + return new FetchableFluentQueryByPredicate<>(this.predicate, this.mappingContext, this.metaData, this.resultType, this.findOperation, + this.countOperation, this.existsOperation, this.sort.and(sort), this.limit, this.properties); + } + + @Override + @SuppressWarnings("HiddenField") + public FetchableFluentQuery limit(int limit) { + return new FetchableFluentQueryByPredicate<>(this.predicate, this.mappingContext, this.metaData, this.resultType, this.findOperation, + this.countOperation, this.existsOperation, this.sort, limit, this.properties); } @Override @SuppressWarnings("HiddenField") public FetchableFluentQuery as(Class resultType) { - return new FetchableFluentQueryByPredicate<>(this.predicate, this.metaData, resultType, this.findOperation, + return new FetchableFluentQueryByPredicate<>(this.predicate, this.mappingContext, this.metaData, resultType, this.findOperation, this.countOperation, this.existsOperation); } @@ -107,8 +124,8 @@ final class FetchableFluentQueryByPredicate extends FluentQuerySupport @SuppressWarnings("HiddenField") public FetchableFluentQuery project(Collection properties) { - return new FetchableFluentQueryByPredicate<>(this.predicate, this.metaData, this.resultType, this.findOperation, - this.countOperation, this.existsOperation, sort, mergeProperties(properties)); + return new FetchableFluentQueryByPredicate<>(this.predicate, this.mappingContext, this.metaData, this.resultType, this.findOperation, + this.countOperation, this.existsOperation, this.sort, this.limit, mergeProperties(properties)); } @Override @@ -117,10 +134,10 @@ final class FetchableFluentQueryByPredicate extends FluentQuerySupport return findOperation.find(metaData.getType()) .as(resultType) .matching( - QueryFragmentsAndParameters.forCondition(metaData, + QueryFragmentsAndParameters.forConditionAndSort(metaData, Cypher.adapt(predicate).asCondition(), - null, - CypherAdapterUtils.toSortItems(this.metaData, sort), + sort, + limit, createIncludedFieldsPredicate())) .oneValue(); } @@ -138,10 +155,10 @@ final class FetchableFluentQueryByPredicate extends FluentQuerySupport return findOperation.find(metaData.getType()) .as(resultType) .matching( - QueryFragmentsAndParameters.forCondition(metaData, + QueryFragmentsAndParameters.forConditionAndSort(metaData, Cypher.adapt(predicate).asCondition(), - null, - CypherAdapterUtils.toSortItems(this.metaData, sort), + sort, + limit, createIncludedFieldsPredicate())) .all(); } @@ -152,9 +169,9 @@ final class FetchableFluentQueryByPredicate extends FluentQuerySupport List page = findOperation.find(metaData.getType()) .as(resultType) .matching( - QueryFragmentsAndParameters.forCondition(metaData, + QueryFragmentsAndParameters.forConditionAndPageable(metaData, Cypher.adapt(predicate).asCondition(), - pageable, null, + pageable, createIncludedFieldsPredicate())) .all(); @@ -162,6 +179,26 @@ final class FetchableFluentQueryByPredicate extends FluentQuerySupport return PageableExecutionUtils.getPage(page, pageable, totalCountSupplier); } + @Override + public Window scroll(ScrollPosition scrollPosition) { + + QueryFragmentsAndParameters queryFragmentsAndParameters = QueryFragmentsAndParameters.forConditionWithScrollPosition(metaData, + Cypher.adapt(predicate).asCondition(), + (scrollPosition instanceof KeysetScrollPosition keysetScrollPosition + ? CypherAdapterUtils.combineKeysetIntoCondition(metaData, keysetScrollPosition, sort, mappingContext.getConversionService()) + : null), + scrollPosition, sort, + limit == null ? 1 : limit + 1, + createIncludedFieldsPredicate()); + + List rawResult = findOperation.find(metaData.getType()) + .as(resultType) + .matching(queryFragmentsAndParameters) + .all(); + + return scroll(scrollPosition, rawResult, metaData); + } + @Override public Stream stream() { return all().stream(); diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/FluentQuerySupport.java b/src/main/java/org/springframework/data/neo4j/repository/query/FluentQuerySupport.java index a83825b81..40ee97fb0 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/FluentQuerySupport.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/FluentQuerySupport.java @@ -18,10 +18,19 @@ package org.springframework.data.neo4j.repository.query; import java.util.Collection; import java.util.Collections; import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; import java.util.Set; +import java.util.function.IntFunction; import java.util.function.Predicate; +import org.springframework.data.domain.KeysetScrollPosition; +import org.springframework.data.domain.OffsetScrollPosition; +import org.springframework.data.domain.ScrollPosition; import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Window; +import org.springframework.data.neo4j.core.mapping.Constants; +import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity; import org.springframework.data.neo4j.core.mapping.PropertyFilter; import org.springframework.lang.Nullable; @@ -38,16 +47,20 @@ abstract class FluentQuerySupport { protected final Sort sort; + protected final Integer limit; + @Nullable protected final Set properties; FluentQuerySupport( Class resultType, Sort sort, + @Nullable Integer limit, @Nullable Collection properties ) { this.resultType = resultType; this.sort = sort; + this.limit = limit; if (properties != null) { this.properties = new HashSet<>(properties); } else { @@ -57,8 +70,8 @@ abstract class FluentQuerySupport { final Predicate createIncludedFieldsPredicate() { - if (this.properties == null) { - return path -> true; + if (this.properties == null || this.properties.isEmpty()) { + return PropertyFilter.NO_FILTER; } return path -> this.properties.contains(path.toDotPath()); } @@ -71,4 +84,49 @@ abstract class FluentQuerySupport { newProperties.addAll(additionalProperties); return Collections.unmodifiableCollection(newProperties); } + + final Window scroll(ScrollPosition scrollPosition, List rawResult, Neo4jPersistentEntity entity) { + + var skip = scrollPosition.isInitial() + ? 0 + : (scrollPosition instanceof OffsetScrollPosition offsetScrollPosition) ? offsetScrollPosition.getOffset() + : 0; + + var scrollDirection = scrollPosition instanceof KeysetScrollPosition keysetScrollPosition ? keysetScrollPosition.getDirection() : ScrollPosition.Direction.FORWARD; + if (scrollDirection == ScrollPosition.Direction.BACKWARD) { + Collections.reverse(rawResult); + } + + IntFunction ding = null; + + if (scrollPosition instanceof OffsetScrollPosition) { + ding = OffsetScrollPosition.positionFunction(skip); + } else { + ding = v -> { + var accessor = entity.getPropertyAccessor(rawResult.get(v)); + var keys = new LinkedHashMap(); + sort.forEach(o -> { + // Storing the graph property name here + var persistentProperty = entity.getRequiredPersistentProperty(o.getProperty()); + keys.put(persistentProperty.getPropertyName(), accessor.getProperty(persistentProperty)); + }); + keys.put(Constants.NAME_OF_ADDITIONAL_SORT, accessor.getProperty(entity.getRequiredIdProperty())); + return ScrollPosition.forward(keys); + }; + } + return Window.from(getSubList(rawResult, limit, scrollDirection), ding, hasMoreElements(rawResult, limit)); + } + + private static boolean hasMoreElements(List result, @Nullable Integer limit) { + return !result.isEmpty() && result.size() > (limit != null ? limit : 0); + } + + private static List getSubList(List result, @Nullable Integer limit, ScrollPosition.Direction scrollDirection) { + + if (limit != null && limit > 0 && result.size() > limit) { + return scrollDirection == ScrollPosition.Direction.FORWARD ? result.subList(0, limit) : result.subList(1, limit + 1); + } + + return result; + } } diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQuerySupport.java b/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQuerySupport.java index 09b9578bb..f1edf93c6 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQuerySupport.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQuerySupport.java @@ -32,7 +32,6 @@ import java.util.function.Supplier; import java.util.stream.Collectors; import org.apache.commons.logging.LogFactory; -import org.neo4j.driver.Value; import org.neo4j.driver.Values; import org.neo4j.driver.types.MapAccessor; import org.neo4j.driver.types.TypeSystem; @@ -292,15 +291,17 @@ abstract class Neo4jQuerySupport { return Window.from(getSubList(rawResult, limit, scrollDirection), v -> { if (scrollPosition instanceof OffsetScrollPosition offsetScrollPosition) { return offsetScrollPosition.advanceBy(v + limit); - } else { + } else { var accessor = neo4jPersistentEntity.getPropertyAccessor(rawResult.get(v)); var keys = new LinkedHashMap(); orderBy.getSort().forEach(o -> { // Storing the graph property name here var persistentProperty = neo4jPersistentEntity.getRequiredPersistentProperty(o.getProperty()); - keys.put(persistentProperty.getPropertyName(), conversionService.convert(accessor.getProperty(persistentProperty), Value.class)); + keys.put(persistentProperty.getPropertyName(), accessor.getProperty(persistentProperty)); + // keys.put(persistentProperty.getPropertyName(), conversionService.convert(accessor.getProperty(persistentProperty), Value.class)); }); - keys.put(Constants.NAME_OF_ADDITIONAL_SORT, conversionService.convert(accessor.getProperty(neo4jPersistentEntity.getRequiredIdProperty()), Value.class)); + keys.put(Constants.NAME_OF_ADDITIONAL_SORT, accessor.getProperty(neo4jPersistentEntity.getRequiredIdProperty())); + // keys.put(Constants.NAME_OF_ADDITIONAL_SORT, conversionService.convert(accessor.getProperty(neo4jPersistentEntity.getRequiredIdProperty()), Value.class)); return ScrollPosition.forward(keys); } }, hasMoreElements(rawResult, limit)); diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/QueryFragmentsAndParameters.java b/src/main/java/org/springframework/data/neo4j/repository/query/QueryFragmentsAndParameters.java index 4f41643a7..7d837500e 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/QueryFragmentsAndParameters.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/QueryFragmentsAndParameters.java @@ -24,7 +24,10 @@ import org.neo4j.cypherdsl.core.PatternElement; import org.neo4j.cypherdsl.core.RelationshipPattern; import org.neo4j.cypherdsl.core.SortItem; import org.springframework.data.domain.Example; +import org.springframework.data.domain.KeysetScrollPosition; +import org.springframework.data.domain.OffsetScrollPosition; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.ScrollPosition; import org.springframework.data.domain.Sort; import org.springframework.data.neo4j.core.mapping.Constants; import org.springframework.data.neo4j.core.mapping.CypherGenerator; @@ -172,32 +175,58 @@ public final class QueryFragmentsAndParameters { return new QueryFragmentsAndParameters(entityMetaData, queryFragments, parameters, null); } + public static QueryFragmentsAndParameters forPageableAndSort(Neo4jPersistentEntity neo4jPersistentEntity, + @Nullable Pageable pageable, @Nullable Sort sort) { + + return getQueryFragmentsAndParameters(neo4jPersistentEntity, pageable, sort, null, null, null, Collections.emptyMap(), null, null, null); + } + /* * Following methods are used by the Simple(Reactive)QueryByExampleExecutor */ static QueryFragmentsAndParameters forExample(Neo4jMappingContext mappingContext, Example example) { - return QueryFragmentsAndParameters.forExample(mappingContext, example, - (java.util.function.Predicate) null); + return forExample(mappingContext, example, null, null, null, null, null, null, null); } - static QueryFragmentsAndParameters forExample(Neo4jMappingContext mappingContext, Example example, @Nullable java.util.function.Predicate includeField) { - return QueryFragmentsAndParameters.forExample(mappingContext, example, null, null, includeField); + static QueryFragmentsAndParameters forExampleWithPageable(Neo4jMappingContext mappingContext, Example example, Pageable pageable, @Nullable java.util.function.Predicate includeField) { + return forExample(mappingContext, example, null, pageable, null, null, null, null, includeField); } - static QueryFragmentsAndParameters forExample(Neo4jMappingContext mappingContext, Example example, Sort sort) { - return QueryFragmentsAndParameters.forExample(mappingContext, example, sort, null); + static QueryFragmentsAndParameters forExampleWithSort(Neo4jMappingContext mappingContext, Example example, Sort sort, @Nullable Integer limit, @Nullable java.util.function.Predicate includeField) { + return forExample(mappingContext, example, null, null, sort, limit, null, null, includeField); } - static QueryFragmentsAndParameters forExample(Neo4jMappingContext mappingContext, Example example, Sort sort, @Nullable java.util.function.Predicate includeField) { - return QueryFragmentsAndParameters.forExample(mappingContext, example, null, sort, includeField); + static QueryFragmentsAndParameters forExampleWithScrollPosition(Neo4jMappingContext mappingContext, Example example, @Nullable Condition keysetScrollPositionCondition, Sort sort, @Nullable Integer limit, @Nullable Long skip, ScrollPosition scrollPosition, @Nullable java.util.function.Predicate includeField) { + return forExample(mappingContext, example, keysetScrollPositionCondition, null, sort, limit, skip, scrollPosition, includeField); } - static QueryFragmentsAndParameters forExample(Neo4jMappingContext mappingContext, Example example, Pageable pageable) { - return QueryFragmentsAndParameters.forExample(mappingContext, example, pageable, null); - } + private static QueryFragmentsAndParameters forExample(Neo4jMappingContext mappingContext, Example example, + @Nullable Condition keysetScrollPositionCondition, + @Nullable Pageable pageable, + @Nullable Sort sort, + @Nullable Integer limit, + @Nullable Long skip, + @Nullable ScrollPosition scrollPosition, + @Nullable java.util.function.Predicate includeField) { - static QueryFragmentsAndParameters forExample(Neo4jMappingContext mappingContext, Example example, Pageable pageable, @Nullable java.util.function.Predicate includeField) { - return QueryFragmentsAndParameters.forExample(mappingContext, example, pageable, null, includeField); + Predicate predicate = Predicate.create(mappingContext, example); + Map parameters = predicate.getParameters(); + Set propertyPathWrappers = predicate.getPropertyPathWrappers(); + Condition condition = predicate.getCondition(); + Neo4jPersistentEntity persistentEntity = mappingContext.getPersistentEntity(example.getProbeType()); + if (scrollPosition instanceof KeysetScrollPosition keysetScrollPosition) { + + if (!keysetScrollPosition.isInitial()) { + condition = condition.and(keysetScrollPositionCondition); + } + QueryFragmentsAndParameters queryFragmentsAndParameters = getQueryFragmentsAndParameters(persistentEntity, pageable, + sort, null, limit, skip, parameters, condition, includeField, propertyPathWrappers); + queryFragmentsAndParameters.getQueryFragments().setRequiresReverseSort(keysetScrollPosition.scrollsBackward()); + return queryFragmentsAndParameters; + } + + return getQueryFragmentsAndParameters(persistentEntity, pageable, + sort, null, limit, skip, parameters, condition, includeField, propertyPathWrappers); } /** @@ -208,78 +237,86 @@ public final class QueryFragmentsAndParameters { * @return Fully populated fragments and parameter */ @API(status = API.Status.EXPERIMENTAL, since = "6.1.7") - public static QueryFragmentsAndParameters forCondition(Neo4jPersistentEntity entityMetaData, Condition condition) { - return forCondition(entityMetaData, condition, null, null); + public static QueryFragmentsAndParameters forCondition(Neo4jPersistentEntity entityMetaData, + Condition condition) { + + return forCondition(entityMetaData, condition, null, null, null, null, null, null); } - static QueryFragmentsAndParameters forCondition(Neo4jPersistentEntity entityMetaData, - Condition condition, - @Nullable Pageable pageable, - @Nullable Collection sortItems - ) { - return forCondition(entityMetaData, condition, pageable, sortItems, null); + static QueryFragmentsAndParameters forConditionAndPageable(Neo4jPersistentEntity entityMetaData, + Condition condition, Pageable pageable, + java.util.function.Predicate includeField) { + + return forCondition(entityMetaData, condition, pageable, null, null, null, null, includeField); } - static QueryFragmentsAndParameters forCondition(Neo4jPersistentEntity entityMetaData, + static QueryFragmentsAndParameters forConditionAndSort(Neo4jPersistentEntity entityMetaData, Condition condition, Sort sort, @Nullable Integer limit, + @Nullable java.util.function.Predicate includeField) { + + return forCondition(entityMetaData, condition, null, sort, null, limit, null, includeField); + } + + static QueryFragmentsAndParameters forConditionAndSortItems(Neo4jPersistentEntity entityMetaData, Condition condition, @Nullable Collection sortItems) { + return forCondition(entityMetaData, condition, null, null, sortItems, null, null, null); + } + + static QueryFragmentsAndParameters forConditionWithScrollPosition(Neo4jPersistentEntity entityMetaData, + Condition condition, + @Nullable Condition keysetCondition, + ScrollPosition scrollPosition, + @Nullable Sort sort, + @Nullable Integer limit, + @Nullable java.util.function.Predicate includeField) { + + long skip = 0L; + + if (scrollPosition instanceof OffsetScrollPosition offsetScrollPosition) { + skip = offsetScrollPosition.isInitial() + ? 0 + : offsetScrollPosition.getOffset(); + + return forCondition(entityMetaData, condition, null, sort, null, limit, skip, includeField); + } + + if (scrollPosition instanceof KeysetScrollPosition keysetScrollPosition) { + if (!scrollPosition.isInitial()) { + condition = condition.and(keysetCondition); + } + QueryFragmentsAndParameters queryFragmentsAndParameters = getQueryFragmentsAndParameters(entityMetaData, null, + sort, null, limit, skip, Collections.emptyMap(), condition, includeField, null); + queryFragmentsAndParameters.getQueryFragments().setRequiresReverseSort(keysetScrollPosition.scrollsBackward()); + return queryFragmentsAndParameters; + } + + throw new IllegalArgumentException("ScrollPosition must be of type OffsetScrollPosition or KeysetScrollPosition. Unexpected type %s found.".formatted(scrollPosition.getClass())); + + } + + // Parameter re-ordering helper + private static QueryFragmentsAndParameters forCondition(Neo4jPersistentEntity entityMetaData, Condition condition, @Nullable Pageable pageable, + @Nullable Sort sort, @Nullable Collection sortItems, + @Nullable Integer limit, + @Nullable Long skip, @Nullable java.util.function.Predicate includeField ) { - QueryFragments queryFragments = new QueryFragments(); - queryFragments.addMatchOn(cypherGenerator.createRootNode(entityMetaData)); - queryFragments.setCondition(condition); - if (includeField == null) { - queryFragments.setReturnExpressions(cypherGenerator.createReturnStatementForMatch(entityMetaData)); - } else { - queryFragments.setReturnExpressions( - cypherGenerator.createReturnStatementForMatch(entityMetaData, includeField)); - } - queryFragments.setRenderConstantsAsParameters(true); - if (pageable != null) { - adaptPageable(entityMetaData, pageable, queryFragments); - } else if (sortItems != null) { - queryFragments.setOrderBy(sortItems); - } - - return new QueryFragmentsAndParameters(entityMetaData, queryFragments, Collections.emptyMap(), null); - } - - private static void adaptPageable( - Neo4jPersistentEntity entityMetaData, - Pageable pageable, - QueryFragments queryFragments - ) { - Sort pageableSort = pageable.getSort(); - queryFragments.setSkip(pageable.getOffset()); - queryFragments.setLimit(pageable.getPageSize()); - queryFragments.setOrderBy(CypherAdapterUtils.toSortItems(entityMetaData, pageableSort)); - } - - static QueryFragmentsAndParameters forExample(Neo4jMappingContext mappingContext, Example example, - @Nullable Pageable pageable, @Nullable Sort sort, java.util.function.Predicate includeField) { - - Predicate predicate = Predicate.create(mappingContext, example); - Map parameters = predicate.getParameters(); - Set propertyPathWrappers = predicate.getPropertyPathWrappers(); - Condition condition = predicate.getCondition(); - - return getQueryFragmentsAndParameters(mappingContext.getPersistentEntity(example.getProbeType()), pageable, - sort, parameters, condition, includeField, propertyPathWrappers); - } - - public static QueryFragmentsAndParameters forPageableAndSort(Neo4jPersistentEntity neo4jPersistentEntity, - @Nullable Pageable pageable, @Nullable Sort sort) { - - return getQueryFragmentsAndParameters(neo4jPersistentEntity, pageable, sort, Collections.emptyMap(), null, null, null); + return getQueryFragmentsAndParameters(entityMetaData, pageable, sort, sortItems, limit, skip, Collections.emptyMap(), condition, includeField, null); } private static QueryFragmentsAndParameters getQueryFragmentsAndParameters( - Neo4jPersistentEntity entityMetaData, @Nullable Pageable pageable, @Nullable Sort sort, - @Nullable Map parameters, @Nullable Condition condition, @Nullable - java.util.function.Predicate includeField, + Neo4jPersistentEntity entityMetaData, + @Nullable Pageable pageable, + @Nullable Sort sort, + @Nullable Collection sortItems, + @Nullable Integer limit, + @Nullable Long skip, + @Nullable Map parameters, + @Nullable Condition condition, + @Nullable java.util.function.Predicate includeField, @Nullable Set propertyPathWrappers) { QueryFragments queryFragments = new QueryFragments(); @@ -305,11 +342,34 @@ public final class QueryFragmentsAndParameters { if (pageable != null) { adaptPageable(entityMetaData, pageable, queryFragments); - } else if (sort != null) { - queryFragments.setOrderBy(CypherAdapterUtils.toSortItems(entityMetaData, sort)); + } else { + if (sort != null) { + queryFragments.setOrderBy(CypherAdapterUtils.toSortItems(entityMetaData, sort)); + } else if (sortItems != null) { + queryFragments.setOrderBy(sortItems); + } + if (limit != null) { + // we don't need to additionally pass the limit to the constructor + // because it will get fetched from the QueryFragments later + queryFragments.setLimit(limit); + } + if (skip != null) { + queryFragments.setSkip(skip); + } } return new QueryFragmentsAndParameters(entityMetaData, queryFragments, parameters, sort); } + private static void adaptPageable( + Neo4jPersistentEntity entityMetaData, + Pageable pageable, + QueryFragments queryFragments + ) { + Sort pageableSort = pageable.getSort(); + queryFragments.setSkip(pageable.getOffset()); + queryFragments.setLimit(pageable.getPageSize()); + queryFragments.setOrderBy(CypherAdapterUtils.toSortItems(entityMetaData, pageableSort)); + } + } diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/QuerydslNeo4jPredicateExecutor.java b/src/main/java/org/springframework/data/neo4j/repository/query/QuerydslNeo4jPredicateExecutor.java index c223997f5..d223e7a45 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/QuerydslNeo4jPredicateExecutor.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/QuerydslNeo4jPredicateExecutor.java @@ -27,6 +27,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.neo4j.core.FluentFindOperation; import org.springframework.data.neo4j.core.Neo4jOperations; +import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity; import org.springframework.data.neo4j.repository.support.CypherdslConditionExecutor; import org.springframework.data.neo4j.repository.support.Neo4jEntityInformation; @@ -65,9 +66,15 @@ public final class QuerydslNeo4jPredicateExecutor implements QuerydslPredicat */ private final Neo4jPersistentEntity metaData; - public QuerydslNeo4jPredicateExecutor(Neo4jEntityInformation entityInformation, - Neo4jOperations neo4jOperations) { + /** + * Mapping context + */ + private final Neo4jMappingContext mappingContext; + public QuerydslNeo4jPredicateExecutor(Neo4jMappingContext mappingContext, Neo4jEntityInformation entityInformation, + Neo4jOperations neo4jOperations) { + + this.mappingContext = mappingContext; this.delegate = new CypherdslConditionExecutorImpl<>(entityInformation, neo4jOperations); this.neo4jOperations = neo4jOperations; this.metaData = entityInformation.getEntityMetaData(); @@ -134,7 +141,7 @@ public final class QuerydslNeo4jPredicateExecutor implements QuerydslPredicat if (this.neo4jOperations instanceof FluentFindOperation ops) { @SuppressWarnings("unchecked") // defaultResultType will be a supertype of S and at this stage, the same. FetchableFluentQuery fluentQuery = - (FetchableFluentQuery) new FetchableFluentQueryByPredicate<>(predicate, metaData, metaData.getType(), + (FetchableFluentQuery) new FetchableFluentQueryByPredicate<>(predicate, mappingContext, metaData, metaData.getType(), ops, this::count, this::exists); return queryFunction.apply(fluentQuery); } diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveCypherdslConditionExecutorImpl.java b/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveCypherdslConditionExecutorImpl.java index a1f7d5272..088cb3cb3 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveCypherdslConditionExecutorImpl.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveCypherdslConditionExecutorImpl.java @@ -18,6 +18,7 @@ package org.springframework.data.neo4j.repository.query; import static org.neo4j.cypherdsl.core.Cypher.asterisk; import java.util.Arrays; +import java.util.function.Predicate; import org.apiguardian.api.API; import org.neo4j.cypherdsl.core.Condition; @@ -29,6 +30,7 @@ import org.springframework.data.domain.Sort; 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.core.mapping.PropertyFilter; import org.springframework.data.neo4j.repository.support.ReactiveCypherdslConditionExecutor; import org.springframework.data.neo4j.repository.support.Neo4jEntityInformation; @@ -63,7 +65,7 @@ public final class ReactiveCypherdslConditionExecutorImpl implements Reactive return this.neo4jOperations.toExecutableQuery( this.metaData.getType(), - QueryFragmentsAndParameters.forCondition(this.metaData, condition, null, null) + QueryFragmentsAndParameters.forCondition(this.metaData, condition) ).flatMap(ReactiveNeo4jOperations.ExecutableQuery::getSingleResult); } @@ -72,17 +74,18 @@ public final class ReactiveCypherdslConditionExecutorImpl implements Reactive return this.neo4jOperations.toExecutableQuery( this.metaData.getType(), - QueryFragmentsAndParameters.forCondition(this.metaData, condition, null, null) + QueryFragmentsAndParameters.forCondition(this.metaData, condition) ).flatMapMany(ReactiveNeo4jOperations.ExecutableQuery::getResults); } @Override public Flux findAll(Condition condition, Sort sort) { + Predicate noFilter = PropertyFilter.NO_FILTER; return this.neo4jOperations.toExecutableQuery( metaData.getType(), - QueryFragmentsAndParameters.forCondition( - this.metaData, condition, null, CypherAdapterUtils.toSortItems(this.metaData, sort) + QueryFragmentsAndParameters.forConditionAndSort( + this.metaData, condition, sort, null, noFilter ) ).flatMapMany(ReactiveNeo4jOperations.ExecutableQuery::getResults); } @@ -92,8 +95,8 @@ public final class ReactiveCypherdslConditionExecutorImpl implements Reactive return this.neo4jOperations.toExecutableQuery( this.metaData.getType(), - QueryFragmentsAndParameters.forCondition( - this.metaData, condition, null, Arrays.asList(sortItems) + QueryFragmentsAndParameters.forConditionAndSortItems( + this.metaData, condition, Arrays.asList(sortItems) ) ).flatMapMany(ReactiveNeo4jOperations.ExecutableQuery::getResults); } @@ -103,7 +106,7 @@ public final class ReactiveCypherdslConditionExecutorImpl implements Reactive return this.neo4jOperations.toExecutableQuery( this.metaData.getType(), - QueryFragmentsAndParameters.forCondition(this.metaData, Conditions.noCondition(), null, + QueryFragmentsAndParameters.forConditionAndSortItems(this.metaData, Conditions.noCondition(), Arrays.asList(sortItems)) ).flatMapMany(ReactiveNeo4jOperations.ExecutableQuery::getResults); } diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveFluentQueryByExample.java b/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveFluentQueryByExample.java index f72a58ff2..658606da1 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveFluentQueryByExample.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveFluentQueryByExample.java @@ -15,6 +15,12 @@ */ package org.springframework.data.neo4j.repository.query; +import org.neo4j.cypherdsl.core.Condition; +import org.springframework.data.domain.KeysetScrollPosition; +import org.springframework.data.domain.OffsetScrollPosition; +import org.springframework.data.domain.ScrollPosition; +import org.springframework.data.domain.Window; +import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -64,7 +70,7 @@ final class ReactiveFluentQueryByExample extends FluentQuerySupport imp Function, Mono> existsOperation ) { this(example, resultType, mappingContext, findOperation, countOperation, existsOperation, Sort.unsorted(), - null); + null, null); } ReactiveFluentQueryByExample( @@ -75,9 +81,10 @@ final class ReactiveFluentQueryByExample extends FluentQuerySupport imp Function, Mono> countOperation, Function, Mono> existsOperation, Sort sort, + @Nullable Integer limit, @Nullable Collection properties ) { - super(resultType, sort, properties); + super(resultType, sort, limit, properties); this.mappingContext = mappingContext; this.example = example; this.findOperation = findOperation; @@ -90,7 +97,15 @@ final class ReactiveFluentQueryByExample extends FluentQuerySupport imp public ReactiveFluentQuery sortBy(Sort sort) { return new ReactiveFluentQueryByExample<>(this.example, this.resultType, this.mappingContext, this.findOperation, - this.countOperation, this.existsOperation, this.sort.and(sort), this.properties); + this.countOperation, this.existsOperation, this.sort.and(sort), this.limit, this.properties); + } + + @Override + @SuppressWarnings("HiddenField") + public ReactiveFluentQuery limit(int limit) { + + return new ReactiveFluentQueryByExample<>(this.example, this.resultType, this.mappingContext, this.findOperation, + this.countOperation, this.existsOperation, this.sort, limit, this.properties); } @Override @@ -106,7 +121,7 @@ final class ReactiveFluentQueryByExample extends FluentQuerySupport imp public ReactiveFluentQuery project(Collection properties) { return new ReactiveFluentQueryByExample<>(this.example, this.resultType, this.mappingContext, this.findOperation, - this.countOperation, this.existsOperation, sort, mergeProperties(properties)); + this.countOperation, this.existsOperation, this.sort, this.limit, mergeProperties(properties)); } @Override @@ -114,7 +129,7 @@ final class ReactiveFluentQueryByExample extends FluentQuerySupport imp return findOperation.find(example.getProbeType()) .as(resultType) - .matching(QueryFragmentsAndParameters.forExample(mappingContext, example, sort, + .matching(QueryFragmentsAndParameters.forExampleWithSort(mappingContext, example, sort, limit, createIncludedFieldsPredicate())) .one(); } @@ -130,7 +145,7 @@ final class ReactiveFluentQueryByExample extends FluentQuerySupport imp return findOperation.find(example.getProbeType()) .as(resultType) - .matching(QueryFragmentsAndParameters.forExample(mappingContext, example, sort, + .matching(QueryFragmentsAndParameters.forExampleWithSort(mappingContext, example, sort, limit, createIncludedFieldsPredicate())) .all(); } @@ -140,7 +155,7 @@ final class ReactiveFluentQueryByExample extends FluentQuerySupport imp Flux results = findOperation.find(example.getProbeType()) .as(resultType) - .matching(QueryFragmentsAndParameters.forExample(mappingContext, example, pageable, + .matching(QueryFragmentsAndParameters.forExampleWithPageable(mappingContext, example, pageable, createIncludedFieldsPredicate())) .all(); return results.collectList().zipWith(countOperation.apply(example)).map(tuple -> { @@ -149,6 +164,28 @@ final class ReactiveFluentQueryByExample extends FluentQuerySupport imp }); } + @Override + public Mono> scroll(ScrollPosition scrollPosition) { + Class domainType = this.example.getProbeType(); + Neo4jPersistentEntity entity = mappingContext.getPersistentEntity(domainType); + + var skip = scrollPosition.isInitial() + ? 0 + : (scrollPosition instanceof OffsetScrollPosition offsetScrollPosition) ? offsetScrollPosition.getOffset() + : 0; + + Condition condition = scrollPosition instanceof KeysetScrollPosition keysetScrollPosition + ? CypherAdapterUtils.combineKeysetIntoCondition(mappingContext.getPersistentEntity(example.getProbeType()), keysetScrollPosition, sort, mappingContext.getConversionService()) + : null; + + return findOperation.find(domainType) + .as(resultType) + .matching(QueryFragmentsAndParameters.forExampleWithScrollPosition(mappingContext, example, condition, sort, limit == null ? 1 : limit + 1, skip, scrollPosition, createIncludedFieldsPredicate())) + .all() + .collectList() + .map(rawResult -> scroll(scrollPosition, rawResult, entity)); + } + @Override public Mono count() { return countOperation.apply(example); 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 index 76cbd9fd8..8e08f74bb 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveFluentQueryByPredicate.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveFluentQueryByPredicate.java @@ -15,6 +15,10 @@ */ package org.springframework.data.neo4j.repository.query; +import org.springframework.data.domain.KeysetScrollPosition; +import org.springframework.data.domain.ScrollPosition; +import org.springframework.data.domain.Window; +import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -57,29 +61,35 @@ import com.querydsl.core.types.Predicate; private final Function> existsOperation; + private final Neo4jMappingContext mappingContext; + ReactiveFluentQueryByPredicate( Predicate predicate, + Neo4jMappingContext mappingContext, Neo4jPersistentEntity metaData, Class resultType, ReactiveFluentFindOperation findOperation, Function> countOperation, Function> existsOperation ) { - this(predicate, metaData, resultType, findOperation, countOperation, existsOperation, Sort.unsorted(), null); + this(predicate, mappingContext, metaData, resultType, findOperation, countOperation, existsOperation, Sort.unsorted(), null, null); } ReactiveFluentQueryByPredicate( Predicate predicate, + Neo4jMappingContext mappingContext, Neo4jPersistentEntity metaData, Class resultType, ReactiveFluentFindOperation findOperation, Function> countOperation, Function> existsOperation, Sort sort, + @Nullable Integer limit, @Nullable Collection properties ) { - super(resultType, sort, properties); + super(resultType, sort, limit, properties); this.predicate = predicate; + this.mappingContext = mappingContext; this.metaData = metaData; this.findOperation = findOperation; this.countOperation = countOperation; @@ -90,15 +100,22 @@ import com.querydsl.core.types.Predicate; @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); + return new ReactiveFluentQueryByPredicate<>(this.predicate, this.mappingContext, this.metaData, this.resultType, this.findOperation, + this.countOperation, this.existsOperation, this.sort.and(sort), this.limit, this.properties); + } + + @Override + @SuppressWarnings("HiddenField") + public ReactiveFluentQuery limit(int limit) { + return new ReactiveFluentQueryByPredicate<>(this.predicate, this.mappingContext, this.metaData, this.resultType, this.findOperation, + this.countOperation, this.existsOperation, this.sort, limit, this.properties); } @Override @SuppressWarnings("HiddenField") public ReactiveFluentQuery as(Class resultType) { - return new ReactiveFluentQueryByPredicate<>(this.predicate, this.metaData, resultType, this.findOperation, + return new ReactiveFluentQueryByPredicate<>(this.predicate, this.mappingContext, this.metaData, resultType, this.findOperation, this.countOperation, this.existsOperation); } @@ -106,8 +123,8 @@ import com.querydsl.core.types.Predicate; @SuppressWarnings("HiddenField") public ReactiveFluentQuery project(Collection properties) { - return new ReactiveFluentQueryByPredicate<>(this.predicate, this.metaData, resultType, this.findOperation, - this.countOperation, this.existsOperation, sort, mergeProperties(properties)); + return new ReactiveFluentQueryByPredicate<>(this.predicate, this.mappingContext, this.metaData, resultType, this.findOperation, + this.countOperation, this.existsOperation, this.sort, this.limit, mergeProperties(properties)); } @Override @@ -116,10 +133,10 @@ import com.querydsl.core.types.Predicate; return findOperation.find(metaData.getType()) .as(resultType) .matching( - QueryFragmentsAndParameters.forCondition(metaData, + QueryFragmentsAndParameters.forConditionAndSort(metaData, Cypher.adapt(predicate).asCondition(), - null, - CypherAdapterUtils.toSortItems(this.metaData, sort), + sort, + limit, createIncludedFieldsPredicate())) .one(); } @@ -136,10 +153,10 @@ import com.querydsl.core.types.Predicate; return findOperation.find(metaData.getType()) .as(resultType) .matching( - QueryFragmentsAndParameters.forCondition(metaData, + QueryFragmentsAndParameters.forConditionAndSort(metaData, Cypher.adapt(predicate).asCondition(), - null, - CypherAdapterUtils.toSortItems(this.metaData, sort), + sort, + limit, createIncludedFieldsPredicate())) .all(); } @@ -150,9 +167,9 @@ import com.querydsl.core.types.Predicate; Flux results = findOperation.find(metaData.getType()) .as(resultType) .matching( - QueryFragmentsAndParameters.forCondition(metaData, + QueryFragmentsAndParameters.forConditionAndPageable(metaData, Cypher.adapt(predicate).asCondition(), - pageable, null, + pageable, createIncludedFieldsPredicate())) .all(); @@ -162,6 +179,25 @@ import com.querydsl.core.types.Predicate; }); } + @Override + public Mono> scroll(ScrollPosition scrollPosition) { + QueryFragmentsAndParameters queryFragmentsAndParameters = QueryFragmentsAndParameters.forConditionWithScrollPosition(metaData, + Cypher.adapt(predicate).asCondition(), + (scrollPosition instanceof KeysetScrollPosition keysetScrollPosition + ? CypherAdapterUtils.combineKeysetIntoCondition(metaData, keysetScrollPosition, sort, mappingContext.getConversionService()) + : null), + scrollPosition, sort, + limit == null ? 1 : limit + 1, + createIncludedFieldsPredicate()); + + return findOperation.find(metaData.getType()) + .as(resultType) + .matching(queryFragmentsAndParameters) + .all() + .collectList() + .map(rawResult -> scroll(scrollPosition, rawResult, metaData)); + } + @Override public Mono count() { return countOperation.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 index b7e7ff7dc..4bc3803b2 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveQuerydslNeo4jPredicateExecutor.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveQuerydslNeo4jPredicateExecutor.java @@ -17,6 +17,7 @@ package org.springframework.data.neo4j.repository.query; import static org.neo4j.cypherdsl.core.Cypher.asterisk; +import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -63,9 +64,15 @@ public final class ReactiveQuerydslNeo4jPredicateExecutor implements Reactive private final Neo4jPersistentEntity metaData; - public ReactiveQuerydslNeo4jPredicateExecutor(Neo4jEntityInformation entityInformation, + /** + * Mapping context + */ + private final Neo4jMappingContext mappingContext; + + public ReactiveQuerydslNeo4jPredicateExecutor(Neo4jMappingContext mappingContext, Neo4jEntityInformation entityInformation, ReactiveNeo4jOperations neo4jOperations) { + this.mappingContext = mappingContext; this.entityInformation = entityInformation; this.neo4jOperations = neo4jOperations; this.metaData = this.entityInformation.getEntityMetaData(); @@ -76,8 +83,7 @@ public final class ReactiveQuerydslNeo4jPredicateExecutor implements Reactive return this.neo4jOperations.toExecutableQuery( this.metaData.getType(), - QueryFragmentsAndParameters.forCondition(this.metaData, Cypher.adapt(predicate).asCondition(), null, - null) + QueryFragmentsAndParameters.forCondition(this.metaData, Cypher.adapt(predicate).asCondition()) ).flatMap(ReactiveNeo4jOperations.ExecutableQuery::getSingleResult); } @@ -108,7 +114,7 @@ public final class ReactiveQuerydslNeo4jPredicateExecutor implements Reactive private Flux doFindAll(Condition condition, Collection sortItems) { return this.neo4jOperations.toExecutableQuery( this.metaData.getType(), - QueryFragmentsAndParameters.forCondition(this.metaData, condition, null, + QueryFragmentsAndParameters.forConditionAndSortItems(this.metaData, condition, sortItems) ).flatMapMany(ReactiveNeo4jOperations.ExecutableQuery::getResults); } @@ -132,7 +138,7 @@ public final class ReactiveQuerydslNeo4jPredicateExecutor implements Reactive if (this.neo4jOperations instanceof ReactiveFluentFindOperation ops) { @SuppressWarnings("unchecked") // defaultResultType will be a supertype of S and at this stage, the same. - ReactiveFluentQuery fluentQuery = (ReactiveFluentQuery) new ReactiveFluentQueryByPredicate<>(predicate, metaData, metaData.getType(), + ReactiveFluentQuery fluentQuery = (ReactiveFluentQuery) new ReactiveFluentQueryByPredicate<>(predicate, mappingContext, metaData, metaData.getType(), ops, this::count, this::exists); return queryFunction.apply(fluentQuery); } diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/SimpleQueryByExampleExecutor.java b/src/main/java/org/springframework/data/neo4j/repository/query/SimpleQueryByExampleExecutor.java index 7970d19c9..7676e0b9c 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/SimpleQueryByExampleExecutor.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/SimpleQueryByExampleExecutor.java @@ -26,6 +26,7 @@ import org.springframework.data.neo4j.core.FluentFindOperation; import org.springframework.data.neo4j.core.Neo4jOperations; import org.springframework.data.neo4j.core.mapping.CypherGenerator; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; +import org.springframework.data.neo4j.core.mapping.PropertyFilter; import org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery; import org.springframework.data.repository.query.QueryByExampleExecutor; import org.springframework.data.support.PageableExecutionUtils; @@ -76,14 +77,14 @@ public final class SimpleQueryByExampleExecutor implements QueryByExampleExec @Override public List findAll(Example example, Sort sort) { return this.neo4jOperations.toExecutableQuery(example.getProbeType(), - QueryFragmentsAndParameters.forExample(mappingContext, example, sort)).getResults(); + QueryFragmentsAndParameters.forExampleWithSort(mappingContext, example, sort, null, PropertyFilter.NO_FILTER)).getResults(); } @Override public Page findAll(Example example, Pageable pageable) { List page = this.neo4jOperations.toExecutableQuery(example.getProbeType(), - QueryFragmentsAndParameters.forExample(mappingContext, example, pageable)).getResults(); + QueryFragmentsAndParameters.forExampleWithPageable(mappingContext, example, pageable, PropertyFilter.NO_FILTER)).getResults(); LongSupplier totalCountSupplier = () -> this.count(example); return PageableExecutionUtils.getPage(page, pageable, totalCountSupplier); diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/SimpleReactiveQueryByExampleExecutor.java b/src/main/java/org/springframework/data/neo4j/repository/query/SimpleReactiveQueryByExampleExecutor.java index 277890ffd..0e4ba361a 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/SimpleReactiveQueryByExampleExecutor.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/SimpleReactiveQueryByExampleExecutor.java @@ -25,6 +25,7 @@ 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.Neo4jMappingContext; +import org.springframework.data.neo4j.core.mapping.PropertyFilter; import org.springframework.data.repository.query.FluentQuery.ReactiveFluentQuery; import org.springframework.data.repository.query.ReactiveQueryByExampleExecutor; import reactor.core.publisher.Flux; @@ -75,7 +76,7 @@ public final class SimpleReactiveQueryByExampleExecutor implements ReactiveQu @Override public Flux findAll(Example example, Sort sort) { return this.neo4jOperations - .toExecutableQuery(example.getProbeType(), QueryFragmentsAndParameters.forExample(mappingContext, example, sort)) + .toExecutableQuery(example.getProbeType(), QueryFragmentsAndParameters.forExampleWithSort(mappingContext, example, sort, null, PropertyFilter.NO_FILTER)) .flatMapMany(ReactiveNeo4jOperations.ExecutableQuery::getResults); } 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 1f4440641..bd3e25d02 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 @@ -90,7 +90,7 @@ final class Neo4jRepositoryFactory extends RepositoryFactorySupport { if (isQueryDslRepository) { - fragments = fragments.append(createDSLExecutorFragment(metadata, QuerydslNeo4jPredicateExecutor.class)); + fragments = fragments.append(createDSLPredicateExecutorFragment(metadata, QuerydslNeo4jPredicateExecutor.class)); } if (CypherdslConditionExecutor.class.isAssignableFrom(metadata.getRepositoryInterface())) { @@ -101,6 +101,14 @@ final class Neo4jRepositoryFactory extends RepositoryFactorySupport { return fragments; } + private RepositoryFragment createDSLPredicateExecutorFragment(RepositoryMetadata metadata, Class implementor) { + + Neo4jEntityInformation entityInformation = getEntityInformation(metadata.getDomainType()); + Object querydslFragment = instantiateClass(implementor, mappingContext, entityInformation, neo4jOperations); + + return RepositoryFragment.implemented(querydslFragment); + } + private RepositoryFragment createDSLExecutorFragment(RepositoryMetadata metadata, Class implementor) { Neo4jEntityInformation entityInformation = getEntityInformation(metadata.getDomainType()); 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 4012c1ed8..450d76bfd 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 @@ -92,7 +92,7 @@ final class ReactiveNeo4jRepositoryFactory extends ReactiveRepositoryFactorySupp if (isQueryDslRepository) { - fragments = fragments.append(createDSLExecutorFragment(metadata, ReactiveQuerydslNeo4jPredicateExecutor.class)); + fragments = fragments.append(createDSLPredicateExecutorFragment(metadata, ReactiveQuerydslNeo4jPredicateExecutor.class)); } if (ReactiveCypherdslConditionExecutor.class.isAssignableFrom(metadata.getRepositoryInterface())) { @@ -103,6 +103,14 @@ final class ReactiveNeo4jRepositoryFactory extends ReactiveRepositoryFactorySupp return fragments; } + private RepositoryFragment createDSLPredicateExecutorFragment(RepositoryMetadata metadata, Class implementor) { + + Neo4jEntityInformation entityInformation = getEntityInformation(metadata.getDomainType()); + Object querydslFragment = instantiateClass(implementor, mappingContext, entityInformation, neo4jOperations); + + return RepositoryFragment.implemented(querydslFragment); + } + private RepositoryFragment createDSLExecutorFragment(RepositoryMetadata metadata, Class implementor) { Neo4jEntityInformation entityInformation = getEntityInformation(metadata.getDomainType()); diff --git a/src/test/java/org/springframework/data/neo4j/integration/imperative/QuerydslNeo4jPredicateExecutorIT.java b/src/test/java/org/springframework/data/neo4j/integration/imperative/QuerydslNeo4jPredicateExecutorIT.java index 9a4e9a5c7..9ffcbb740 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/imperative/QuerydslNeo4jPredicateExecutorIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/imperative/QuerydslNeo4jPredicateExecutorIT.java @@ -18,9 +18,11 @@ package org.springframework.data.neo4j.integration.imperative; import static org.assertj.core.api.Assertions.assertThat; import java.util.List; +import java.util.Map; import java.util.stream.Stream; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.neo4j.driver.Driver; import org.neo4j.driver.Session; @@ -28,9 +30,12 @@ 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.KeysetScrollPosition; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.ScrollPosition; import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Window; import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration; import org.springframework.data.neo4j.core.DatabaseSelectionProvider; import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager; @@ -126,6 +131,96 @@ class QuerydslNeo4jPredicateExecutorIT { }); } + @Test + @Tag("GH-2726") + void scrollByExampleWithNoOffset(@Autowired QueryDSLPersonRepository repository) { + Predicate predicate = Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")) + .or(Expressions.predicate(Ops.EQ, lastNamePath, Expressions.asString("B."))); + + Window peopleWindow = repository.findBy(predicate, q -> q.limit(1).scroll(ScrollPosition.offset(0))); + + assertThat(peopleWindow.getContent()).extracting(Person::getFirstName) + .containsExactlyInAnyOrder("Helge"); + + assertThat(peopleWindow.isLast()).isFalse(); + assertThat(peopleWindow.hasNext()).isTrue(); + + assertThat(peopleWindow.positionAt(peopleWindow.getContent().get(0))).isEqualTo(ScrollPosition.offset(1)); + } + + @Test + @Tag("GH-2726") + void scrollByExampleWithOffset(@Autowired QueryDSLPersonRepository repository) { + Predicate predicate = Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")) + .or(Expressions.predicate(Ops.EQ, lastNamePath, Expressions.asString("B."))); + + Window peopleWindow = repository.findBy(predicate, q -> q.limit(1).scroll(ScrollPosition.offset(1))); + + assertThat(peopleWindow.getContent()).extracting(Person::getFirstName) + .containsExactlyInAnyOrder("Bela"); + + assertThat(peopleWindow.isLast()).isTrue(); + + assertThat(peopleWindow.positionAt(peopleWindow.getContent().get(0))).isEqualTo(ScrollPosition.offset(2)); + } + + @Test + @Tag("GH-2726") + void scrollByExampleWithContinuingOffset(@Autowired QueryDSLPersonRepository repository) { + Predicate predicate = Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")) + .or(Expressions.predicate(Ops.EQ, lastNamePath, Expressions.asString("B."))); + + Window peopleWindow = repository.findBy(predicate, q -> q.limit(1).scroll(ScrollPosition.offset(0))); + ScrollPosition currentPosition = peopleWindow.positionAt(peopleWindow.getContent().get(0)); + peopleWindow = repository.findBy(predicate, q -> q.limit(1).scroll(currentPosition)); + + assertThat(peopleWindow.getContent()).extracting(Person::getFirstName) + .containsExactlyInAnyOrder("Bela"); + + assertThat(peopleWindow.isLast()).isTrue(); + } + + @Test + @Tag("GH-2726") + void scrollByExampleWithKeysetOffset(@Autowired QueryDSLPersonRepository repository) { + Predicate predicate = Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")) + .or(Expressions.predicate(Ops.EQ, lastNamePath, Expressions.asString("B."))); + + Window peopleWindow = repository.findBy(predicate, q -> q.sortBy(Sort.by("firstName")).limit(1).scroll(ScrollPosition.keyset())); + assertThat(peopleWindow.getContent()).extracting(Person::getFirstName) + .containsExactly("Bela"); + + ScrollPosition currentPosition = peopleWindow.positionAt(peopleWindow.size() - 1); + peopleWindow = repository.findBy(predicate, q -> q.limit(1).scroll(currentPosition)); + + assertThat(peopleWindow.getContent()).extracting(Person::getFirstName) + .containsExactlyInAnyOrder("Helge"); + + assertThat(peopleWindow.isLast()).isTrue(); + } + + @Test + @Tag("GH-2726") + void scrollByExampleWithKeysetOffsetBackward(@Autowired QueryDSLPersonRepository repository) { + Predicate predicate = Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")) + .or(Expressions.predicate(Ops.EQ, lastNamePath, Expressions.asString("B."))); + + KeysetScrollPosition startPosition = ScrollPosition.backward(Map.of( + "lastName", "Schneider" + )); + Window peopleWindow = repository.findBy(predicate, q -> q.sortBy(Sort.by("firstName")).limit(1).scroll(startPosition)); + assertThat(peopleWindow.getContent()).extracting(Person::getFirstName) + .containsExactly("Helge"); + + var nextPos = ScrollPosition.backward( + ((KeysetScrollPosition) peopleWindow.positionAt(0)).getKeys()); + + peopleWindow = repository.findBy(predicate, q -> q.limit(1).scroll(nextPos)); + + assertThat(peopleWindow.getContent()).extracting(Person::getFirstName) + .containsExactlyInAnyOrder("Bela"); + } + static class DtoPersonProjection { private final String firstName; @@ -222,6 +317,18 @@ class QuerydslNeo4jPredicateExecutorIT { assertThat(count).isEqualTo(2); } + @Test // GH-2726 + void fluentFindAllWithLimitShouldWork(@Autowired QueryDSLPersonRepository repository) { + + Predicate predicate = Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")) + .or(Expressions.predicate(Ops.EQ, lastNamePath, Expressions.asString("B."))); + List people = repository.findBy(predicate, + q -> q.limit(1)).all(); + + assertThat(people).hasSize(1); + assertThat(people).extracting(Person::getFirstName).containsExactly("Helge"); + } + @Test void findOneShouldWork(@Autowired QueryDSLPersonRepository repository) { diff --git a/src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryIT.java b/src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryIT.java index 45acf5d6a..97ca60fa0 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryIT.java @@ -84,6 +84,7 @@ import org.springframework.data.domain.Range.Bound; import org.springframework.data.domain.ScrollPosition; import org.springframework.data.domain.Slice; import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Window; import org.springframework.data.geo.Box; import org.springframework.data.geo.Circle; import org.springframework.data.geo.Distance; @@ -2880,6 +2881,23 @@ class RepositoryIT { assertThat(person).isEqualTo(person1); } + @Test // GH-2726 + void scrollByExample(@Autowired PersonRepository repository) { + + PersonWithAllConstructor sameValuePerson = new PersonWithAllConstructor(null, null, null, TEST_PERSON_SAMEVALUE, null, null, null, null, null, null, null); + + Example example = Example.of(sameValuePerson, + ExampleMatcher.matchingAll().withIgnoreNullValues()); + Window person = repository.findBy(example, q -> q.sortBy(Sort.by("name")).limit(1).scroll(ScrollPosition.offset(0))); + + assertThat(person).isNotNull(); + assertThat(person.getContent().get(0)).isEqualTo(person1); + + ScrollPosition currentPosition = person.positionAt(person1); + person = repository.findBy(example, q -> q.sortBy(Sort.by("name")).limit(1).scroll(currentPosition)); + assertThat(person.getContent().get(0)).isEqualTo(person2); + } + @Test void findAllByExampleWithDifferentMatchers(@Autowired PersonRepository repository) { @@ -2964,6 +2982,16 @@ class RepositoryIT { assertThat(persons).containsExactly(person2); } + @Test // GH-2726 + void findAllByExampleWithScrollFluent(@Autowired PersonRepository repository) { + + Example example = Example.of(personExample(TEST_PERSON_SAMEVALUE)); + Window persons = repository.findBy(example, + q -> q.sortBy(Sort.by("name")).limit(1).scroll(ScrollPosition.keyset().forward())); + + assertThat(persons.getContent()).containsExactly(person1); + } + @Test void existsByExample(@Autowired PersonRepository repository) { diff --git a/src/test/java/org/springframework/data/neo4j/integration/imperative/ScrollingIT.java b/src/test/java/org/springframework/data/neo4j/integration/imperative/ScrollingIT.java index ca588361d..7bdec2625 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/imperative/ScrollingIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/imperative/ScrollingIT.java @@ -23,12 +23,15 @@ import java.util.function.Function; import org.assertj.core.data.Index; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.neo4j.driver.Driver; import org.neo4j.driver.Values; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.data.domain.Example; +import org.springframework.data.domain.ExampleMatcher; import org.springframework.data.domain.KeysetScrollPosition; import org.springframework.data.domain.ScrollPosition; import org.springframework.data.neo4j.core.DatabaseSelectionProvider; @@ -107,6 +110,32 @@ class ScrollingIT { .containsExactly("H0", "I0"); } + @Test + @Tag("GH-2726") + void forwardWithFluentQueryByExample(@Autowired ScrollingRepository scrollingRepository) { + ScrollingEntity scrollingEntity = new ScrollingEntity(); + Example example = Example.of(scrollingEntity, ExampleMatcher.matchingAll().withIgnoreNullValues()); + + var window = scrollingRepository.findBy(example, q -> q.sortBy(ScrollingEntity.SORT_BY_C).limit(4).scroll(ScrollPosition.keyset())); + assertThat(window.hasNext()).isTrue(); + assertThat(window) + .hasSize(4) + .extracting(ScrollingEntity::getA) + .containsExactly("A0", "B0", "C0", "D0"); + + ScrollPosition newPosition = ScrollPosition.forward(((KeysetScrollPosition) window.positionAt(window.size() - 1)).getKeys()); + window = scrollingRepository.findBy(example, q -> q.sortBy(ScrollingEntity.SORT_BY_C).limit(4).scroll(newPosition)); + assertThat(window) + .hasSize(4) + .extracting(ScrollingEntity::getA) + .containsExactly("D0", "E0", "F0", "G0"); + + window = scrollingRepository.findTop4By(ScrollingEntity.SORT_BY_C, window.positionAt(window.size() - 1)); + assertThat(window.isLast()).isTrue(); + assertThat(window).extracting(ScrollingEntity::getA) + .containsExactly("H0", "I0"); + } + @Test void forwardWithDuplicatesIteratorIteration(@Autowired ScrollingRepository repository) { @@ -162,6 +191,42 @@ class ScrollingIT { .containsExactly("A0", "B0"); } + @Test + void backwardWithFluentQueryByExample(@Autowired ScrollingRepository repository) { + + ScrollingEntity scrollingEntity = new ScrollingEntity(); + Example example = Example.of(scrollingEntity, ExampleMatcher.matchingAll().withIgnoreNullValues()); + + var last = repository.findFirstByA("I0"); + var keys = Map.of( + "c", last.getC(), + Constants.NAME_OF_ADDITIONAL_SORT, Values.value(last.getId().toString()) + ); + + var window = repository.findBy(example, q -> q.sortBy(ScrollingEntity.SORT_BY_C).limit(4).scroll(ScrollPosition.backward(keys))); + assertThat(window.hasNext()).isTrue(); + assertThat(window) + .hasSize(4) + .extracting(ScrollingEntity::getA) + .containsExactly("F0", "G0", "H0", "I0"); + + var pos = ((KeysetScrollPosition) window.positionAt(0)); + var nextPos = ScrollPosition.backward(pos.getKeys()); + window = repository.findBy(example, q -> q.sortBy(ScrollingEntity.SORT_BY_C).limit(4).scroll(nextPos)); + assertThat(window.hasNext()).isTrue(); + assertThat(window) + .hasSize(4) + .extracting(Function.identity()) + .extracting(ScrollingEntity::getA) + .containsExactly("C0", "D0", "D0", "E0"); + + var nextNextPos = ScrollPosition.backward(((KeysetScrollPosition) window.positionAt(0)).getKeys()); + window = repository.findBy(example, q -> q.sortBy(ScrollingEntity.SORT_BY_C).limit(4).scroll(nextNextPos)); + assertThat(window.isLast()).isTrue(); + assertThat(window).extracting(ScrollingEntity::getA) + .containsExactly("A0", "B0"); + } + @Configuration @EnableNeo4jRepositories @EnableTransactionManagement 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 index d621a68ab..af1f461f1 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveQuerydslNeo4jPredicateExecutorIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveQuerydslNeo4jPredicateExecutorIT.java @@ -17,6 +17,8 @@ package org.springframework.data.neo4j.integration.reactive; import static org.assertj.core.api.Assertions.assertThat; +import org.junit.jupiter.api.Tag; +import org.springframework.data.domain.ScrollPosition; import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration; import reactor.test.StepVerifier; @@ -188,6 +190,70 @@ class ReactiveQuerydslNeo4jPredicateExecutorIT { }).verifyComplete(); } + @Test + @Tag("GH-2726") + void scrollByExampleWithNoOffset(@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.limit(1).scroll(ScrollPosition.offset(0))) + .as(StepVerifier::create) + .expectNextMatches(peopleWindow -> { + + assertThat(peopleWindow.getContent()).extracting(Person::getFirstName) + .containsExactlyInAnyOrder("Helge"); + + assertThat(peopleWindow.isLast()).isFalse(); + assertThat(peopleWindow.hasNext()).isTrue(); + + assertThat(peopleWindow.positionAt(peopleWindow.getContent().get(0))).isEqualTo(ScrollPosition.offset(1)); + return true; + }).verifyComplete(); + } + + @Test + @Tag("GH-2726") + void scrollByExampleWithOffset(@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.limit(1).scroll(ScrollPosition.offset(1))) + .as(StepVerifier::create) + .expectNextMatches(peopleWindow -> { + assertThat(peopleWindow.getContent()).extracting(Person::getFirstName) + .containsExactlyInAnyOrder("Bela"); + + assertThat(peopleWindow.isLast()).isTrue(); + assertThat(peopleWindow.positionAt(peopleWindow.getContent().get(0))).isEqualTo(ScrollPosition.offset(2)); + return true; + }).verifyComplete(); + } + + @Test + @Tag("GH-2726") + void scrollByExampleWithContinuingOffset(@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.limit(1).scroll(ScrollPosition.offset(0))) + .as(StepVerifier::create) + .expectNextMatches(peopleWindow -> { + ScrollPosition currentPosition = peopleWindow.positionAt(peopleWindow.getContent().get(0)); + repository.findBy(predicate, q -> q.limit(1).scroll(currentPosition)) + .as(StepVerifier::create) + .expectNextMatches(nextPeopleWindow -> { + + assertThat(nextPeopleWindow.getContent()).extracting(Person::getFirstName) + .containsExactlyInAnyOrder("Bela"); + + assertThat(nextPeopleWindow.isLast()).isTrue(); + return true; + }); + return true; + }); + + } + @Test // GH-2361 void fluentExistsShouldWork(@Autowired QueryDSLPersonRepository repository) { diff --git a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveRepositoryIT.java b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveRepositoryIT.java index e2df46d75..da263ad5c 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveRepositoryIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveRepositoryIT.java @@ -60,6 +60,7 @@ import org.springframework.core.convert.ConverterNotFoundException; import org.springframework.data.domain.Example; import org.springframework.data.domain.ExampleMatcher; import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.ScrollPosition; import org.springframework.data.domain.Sort; import org.springframework.data.mapping.MappingException; import org.springframework.data.neo4j.core.DatabaseSelection; @@ -360,6 +361,30 @@ class ReactiveRepositoryIT { .verifyComplete(); } + @Test // GH-2726 + void scrollByExample(@Autowired ReactivePersonRepository repository) { + + PersonWithAllConstructor sameValuePerson = new PersonWithAllConstructor(null, null, null, TEST_PERSON_SAMEVALUE, null, null, null, null, null, null, null); + + Example example = Example.of(sameValuePerson, + ExampleMatcher.matchingAll().withIgnoreNullValues()); + repository.findBy(example, q -> q.sortBy(Sort.by("name")).limit(1).scroll(ScrollPosition.offset(0))) + .as(StepVerifier::create) + .expectNextMatches(person -> { + assertThat(person).isNotNull(); + assertThat(person.getContent().get(0)).isEqualTo(person1); + + ScrollPosition currentPosition = person.positionAt(person1); + repository.findBy(example, q -> q.sortBy(Sort.by("name")).limit(1).scroll(currentPosition)) + .as(StepVerifier::create) + .expectNextMatches(nextPerson -> { + assertThat(nextPerson.getContent().get(0)).isEqualTo(person2); + return true; + }); + return true; + }); + } + @Test void findAllByExampleWithDifferentMatchers(@Autowired ReactivePersonRepository repository) { PersonWithAllConstructor person; diff --git a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveScrollingIT.java b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveScrollingIT.java index e0d3a28ce..51b2e2cd6 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveScrollingIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveScrollingIT.java @@ -24,12 +24,15 @@ import java.util.function.Function; import org.assertj.core.data.Index; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.neo4j.driver.Driver; import org.neo4j.driver.Values; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.data.domain.Example; +import org.springframework.data.domain.ExampleMatcher; import org.springframework.data.domain.KeysetScrollPosition; import org.springframework.data.domain.ScrollPosition; import org.springframework.data.domain.Window; @@ -180,6 +183,96 @@ class ReactiveScrollingIT { .containsExactly("A0", "B0"); } + @Test + @Tag("GH-2726") + void forwardWithFluentQueryByExample(@Autowired ReactiveScrollingRepository repository) { + ScrollingEntity scrollingEntity = new ScrollingEntity(); + Example example = Example.of(scrollingEntity, ExampleMatcher.matchingAll().withIgnoreNullValues()); + + var windowContainer = new AtomicReference>(); + repository.findBy(example, q -> q.sortBy(ScrollingEntity.SORT_BY_C).limit(4).scroll(ScrollPosition.keyset())) + .as(StepVerifier::create) + .consumeNextWith(windowContainer::set) + .verifyComplete(); + var window = windowContainer.get(); + assertThat(window.hasNext()).isTrue(); + assertThat(window) + .hasSize(4) + .extracting(ScrollingEntity::getA) + .containsExactly("A0", "B0", "C0", "D0"); + + ScrollPosition nextScrollPosition = window.positionAt(window.size() - 1); + repository.findBy(example, q -> q.sortBy(ScrollingEntity.SORT_BY_C).limit(4).scroll(nextScrollPosition)) + .as(StepVerifier::create) + .consumeNextWith(windowContainer::set) + .verifyComplete(); + window = windowContainer.get(); + assertThat(window.hasNext()).isTrue(); + assertThat(window) + .hasSize(4) + .extracting(ScrollingEntity::getA) + .containsExactly("D0", "E0", "F0", "G0"); + + ScrollPosition nextNextScrollPosition = window.positionAt(window.size() - 1); + repository.findBy(example, q -> q.sortBy(ScrollingEntity.SORT_BY_C).limit(4).scroll(nextNextScrollPosition)) + .as(StepVerifier::create) + .consumeNextWith(windowContainer::set) + .verifyComplete(); + window = windowContainer.get(); + assertThat(window.isLast()).isTrue(); + assertThat(window).extracting(ScrollingEntity::getA) + .containsExactly("H0", "I0"); + } + + @Test + @Tag("GH-2726") + void backwardWithFluentQueryByExample(@Autowired ReactiveScrollingRepository repository) { + + Example example = Example.of(new ScrollingEntity(), ExampleMatcher.matchingAll().withIgnoreNullValues()); + + // Recreate the last position + var last = repository.findFirstByA("I0").block(); + var keys = Map.of( + "c", Values.value(last.getC()), + Constants.NAME_OF_ADDITIONAL_SORT, Values.value(last.getId().toString()) + ); + + var windowContainer = new AtomicReference>(); + repository.findBy(example, q -> q.sortBy(ScrollingEntity.SORT_BY_C).limit(4).scroll(ScrollPosition.backward(keys))) + .as(StepVerifier::create) + .consumeNextWith(windowContainer::set) + .verifyComplete(); + var window = windowContainer.get(); + assertThat(window.hasNext()).isTrue(); + assertThat(window) + .hasSize(4) + .extracting(ScrollingEntity::getA) + .containsExactly("F0", "G0", "H0", "I0"); + + var nextPos = ScrollPosition.backward(((KeysetScrollPosition) window.positionAt(0)).getKeys()); + repository.findBy(example, q -> q.sortBy(ScrollingEntity.SORT_BY_C).limit(4).scroll(nextPos)) + .as(StepVerifier::create) + .consumeNextWith(windowContainer::set) + .verifyComplete(); + window = windowContainer.get(); + assertThat(window.hasNext()).isTrue(); + assertThat(window) + .hasSize(4) + .extracting(Function.identity()) + .extracting(ScrollingEntity::getA) + .containsExactly("C0", "D0", "D0", "E0"); + + var nextNextPos = ScrollPosition.backward(((KeysetScrollPosition) window.positionAt(0)).getKeys()); + repository.findBy(example, q -> q.sortBy(ScrollingEntity.SORT_BY_C).limit(4).scroll(nextNextPos)) + .as(StepVerifier::create) + .consumeNextWith(windowContainer::set) + .verifyComplete(); + window = windowContainer.get(); + assertThat(window.isLast()).isTrue(); + assertThat(window).extracting(ScrollingEntity::getA) + .containsExactly("A0", "B0"); + } + @Configuration @EnableNeo4jRepositories @EnableReactiveNeo4jRepositories diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ScrollingEntity.java b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ScrollingEntity.java index 4d991f82c..b995fcd65 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/common/ScrollingEntity.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/common/ScrollingEntity.java @@ -37,6 +37,7 @@ public class ScrollingEntity { * Sorting by b and a will not be unique for 3 and D0, so this will trigger the additional condition based on the id */ public static final Sort SORT_BY_B_AND_A = Sort.by(Sort.Order.asc("b"), Sort.Order.desc("a")); + public static final Sort SORT_BY_C = Sort.by(Sort.Order.asc("c")); public static void createTestData(QueryRunner queryRunner) { queryRunner.run("MATCH (n) DETACH DELETE n"); diff --git a/src/test/java/org/springframework/data/neo4j/repository/query/CypherAdapterUtilsTest.java b/src/test/java/org/springframework/data/neo4j/repository/query/CypherAdapterUtilsTest.java index b8b64b1ae..bd5d1287e 100644 --- a/src/test/java/org/springframework/data/neo4j/repository/query/CypherAdapterUtilsTest.java +++ b/src/test/java/org/springframework/data/neo4j/repository/query/CypherAdapterUtilsTest.java @@ -44,7 +44,7 @@ class CypherAdapterUtilsTest { var condition = CypherAdapterUtils.combineKeysetIntoCondition(entity, ScrollPosition.forward(Map.of("foobar", "D0", "b", 3, "c", LocalDateTime.of(2023, 3, 19, 14, 21, 8, 716))), - Sort.by(Sort.Order.asc("b"), Sort.Order.desc("a"), Sort.Order.asc("c")) + Sort.by(Sort.Order.asc("b"), Sort.Order.desc("a"), Sort.Order.asc("c")), mappingContext.getConversionService() ); var expected = """