4545import org .neo4j .cypherdsl .core .RelationshipPattern ;
4646import org .neo4j .cypherdsl .core .SortItem ;
4747import org .neo4j .driver .types .Point ;
48+ import org .springframework .data .domain .KeysetScrollPosition ;
49+ import org .springframework .data .domain .OffsetScrollPosition ;
4850import org .springframework .data .domain .Pageable ;
4951import org .springframework .data .domain .Range ;
52+ import org .springframework .data .domain .ScrollPosition ;
5053import org .springframework .data .domain .Sort ;
5154import org .springframework .data .geo .Box ;
5255import org .springframework .data .geo .Circle ;
6467import org .springframework .data .neo4j .core .mapping .PropertyFilter ;
6568import org .springframework .data .neo4j .core .mapping .RelationshipDescription ;
6669import org .springframework .data .neo4j .core .schema .TargetNode ;
70+ import org .springframework .data .repository .query .QueryMethod ;
6771import org .springframework .data .repository .query .parser .AbstractQueryCreator ;
6872import org .springframework .data .repository .query .parser .Part ;
6973import org .springframework .data .repository .query .parser .PartTree ;
8286final class CypherQueryCreator extends AbstractQueryCreator <QueryFragmentsAndParameters , Condition > {
8387
8488 private final Neo4jMappingContext mappingContext ;
89+ private final QueryMethod queryMethod ;
8590
8691 private final Class <?> domainType ;
8792 private final NodeDescription <?> nodeDescription ;
@@ -99,6 +104,8 @@ final class CypherQueryCreator extends AbstractQueryCreator<QueryFragmentsAndPar
99104
100105 private final Pageable pagingParameter ;
101106
107+ private final ScrollPosition scrollPosition ;
108+
102109 /**
103110 * Stores the number of max results, if the {@link PartTree tree} is limiting.
104111 */
@@ -113,18 +120,21 @@ final class CypherQueryCreator extends AbstractQueryCreator<QueryFragmentsAndPar
113120
114121 private final List <PropertyPathWrapper > propertyPathWrappers ;
115122
123+ private final boolean keysetRequiresSort ;
124+
116125 /**
117126 * Can be used to modify the limit of a paged or sliced query.
118127 */
119128 private final UnaryOperator <Integer > limitModifier ;
120129
121- CypherQueryCreator (Neo4jMappingContext mappingContext , Class <?> domainType , Neo4jQueryType queryType , PartTree tree ,
130+ CypherQueryCreator (Neo4jMappingContext mappingContext , QueryMethod queryMethod , Class <?> domainType , Neo4jQueryType queryType , PartTree tree ,
122131 Neo4jParameterAccessor actualParameters , Collection <PropertyFilter .ProjectedPath > includedProperties ,
123132 BiFunction <Object , Neo4jPersistentPropertyConverter <?>, Object > parameterConversion ,
124133 UnaryOperator <Integer > limitModifier ) {
125134
126135 super (tree , actualParameters );
127136 this .mappingContext = mappingContext ;
137+ this .queryMethod = queryMethod ;
128138
129139 this .domainType = domainType ;
130140 this .nodeDescription = this .mappingContext .getRequiredNodeDescription (this .domainType );
@@ -139,6 +149,7 @@ final class CypherQueryCreator extends AbstractQueryCreator<QueryFragmentsAndPar
139149 this .parameterConversion = parameterConversion ;
140150
141151 this .pagingParameter = actualParameters .getPageable ();
152+ this .scrollPosition = actualParameters .getScrollPosition ();
142153 this .limitModifier = limitModifier ;
143154
144155 AtomicInteger symbolicNameIndex = new AtomicInteger ();
@@ -148,6 +159,7 @@ final class CypherQueryCreator extends AbstractQueryCreator<QueryFragmentsAndPar
148159 mappingContext .getPersistentPropertyPath (part .getProperty ())))
149160 .collect (Collectors .toList ());
150161
162+ this .keysetRequiresSort = queryMethod .isScrollQuery () && actualParameters .getScrollPosition () instanceof KeysetScrollPosition ;
151163 }
152164
153165 private class PropertyPathWrapper {
@@ -260,7 +272,12 @@ protected QueryFragmentsAndParameters complete(@Nullable Condition condition, So
260272 .collect (Collectors .toMap (p -> p .nameOrIndex , p -> parameterConversion .apply (p .value , p .conversionOverride )));
261273
262274 QueryFragments queryFragments = createQueryFragments (condition , sort );
263- return new QueryFragmentsAndParameters (nodeDescription , queryFragments , convertedParameters );
275+
276+ var theSort = pagingParameter .getSort ().and (sort );
277+ if (keysetRequiresSort && theSort .isUnsorted ()) {
278+ throw new UnsupportedOperationException ("Unsorted keyset based scrolling is not supported." );
279+ }
280+ return new QueryFragmentsAndParameters (nodeDescription , queryFragments , convertedParameters , theSort );
264281 }
265282
266283 @ NonNull
@@ -280,15 +297,12 @@ private QueryFragments createQueryFragments(@Nullable Condition condition, Sort
280297 }
281298 }
282299
283- // closing action: add the condition and path match
284- queryFragments .setCondition (conditionFragment );
285-
286300 if (!relationshipChain .isEmpty ()) {
287301 queryFragments .setMatchOn (relationshipChain );
288302 } else {
289303 queryFragments .addMatchOn (startNode );
290304 }
291- /// end of initial filter query creation
305+ // end of initial filter query creation
292306
293307 if (queryType == Neo4jQueryType .COUNT ) {
294308 queryFragments .setReturnExpression (Functions .count (Cypher .asterisk ()), true );
@@ -298,20 +312,38 @@ private QueryFragments createQueryFragments(@Nullable Condition condition, Sort
298312 queryFragments .setDeleteExpression (Constants .NAME_OF_TYPED_ROOT_NODE .apply (nodeDescription ));
299313 queryFragments .setReturnExpression (Functions .count (Constants .NAME_OF_TYPED_ROOT_NODE .apply (nodeDescription )), true );
300314 } else {
315+
316+ var theSort = pagingParameter .getSort ().and (sort );
317+
318+ if (pagingParameter .isUnpaged () && scrollPosition == null && maxResults != null ) {
319+ queryFragments .setLimit (limitModifier .apply (maxResults .intValue ()));
320+ } else if (scrollPosition instanceof KeysetScrollPosition keysetScrollPosition ) {
321+
322+ Neo4jPersistentEntity <?> entity = (Neo4jPersistentEntity <?>) nodeDescription ;
323+ // Enforce sorting by something that is hopefully stable comparable (looking at Neo4j's id() with tears in my eyes).
324+ theSort = theSort .and (Sort .by (entity .getRequiredIdProperty ().getName ()).ascending ());
325+
326+ queryFragments .setLimit (limitModifier .apply (maxResults .intValue ()));
327+ if (!keysetScrollPosition .isInitial ()) {
328+ conditionFragment = conditionFragment .and (CypherAdapterUtils .combineKeysetIntoCondition (entity , keysetScrollPosition , theSort ));
329+ }
330+
331+ queryFragments .setRequiresReverseSort (keysetScrollPosition .getDirection () == KeysetScrollPosition .Direction .Backward );
332+ } else if (scrollPosition instanceof OffsetScrollPosition offsetScrollPosition ) {
333+ queryFragments .setSkip (offsetScrollPosition .getOffset ());
334+ queryFragments .setLimit (limitModifier .apply (pagingParameter .isUnpaged () ? maxResults .intValue () : pagingParameter .getPageSize ()));
335+ }
336+
301337 queryFragments .setReturnBasedOn (nodeDescription , includedProperties , isDistinct );
302338 queryFragments .setOrderBy (Stream
303339 .concat (sortItems .stream (),
304- pagingParameter . getSort (). and ( sort ) .stream ().map (CypherAdapterUtils .sortAdapterFor (nodeDescription )))
340+ theSort .stream ().map (CypherAdapterUtils .sortAdapterFor (nodeDescription )))
305341 .collect (Collectors .toList ()));
306- if (pagingParameter .isUnpaged ()) {
307- queryFragments .setLimit (maxResults );
308- } else {
309- long skip = pagingParameter .getOffset ();
310- int pageSize = pagingParameter .getPageSize ();
311- queryFragments .setSkip (skip );
312- queryFragments .setLimit (limitModifier .apply (pageSize ));
313- }
314342 }
343+
344+ // closing action: add the condition and path match
345+ queryFragments .setCondition (conditionFragment );
346+
315347 return queryFragments ;
316348 }
317349
0 commit comments