45
45
import org .neo4j .cypherdsl .core .RelationshipPattern ;
46
46
import org .neo4j .cypherdsl .core .SortItem ;
47
47
import org .neo4j .driver .types .Point ;
48
+ import org .springframework .data .domain .KeysetScrollPosition ;
49
+ import org .springframework .data .domain .OffsetScrollPosition ;
48
50
import org .springframework .data .domain .Pageable ;
49
51
import org .springframework .data .domain .Range ;
52
+ import org .springframework .data .domain .ScrollPosition ;
50
53
import org .springframework .data .domain .Sort ;
51
54
import org .springframework .data .geo .Box ;
52
55
import org .springframework .data .geo .Circle ;
64
67
import org .springframework .data .neo4j .core .mapping .PropertyFilter ;
65
68
import org .springframework .data .neo4j .core .mapping .RelationshipDescription ;
66
69
import org .springframework .data .neo4j .core .schema .TargetNode ;
70
+ import org .springframework .data .repository .query .QueryMethod ;
67
71
import org .springframework .data .repository .query .parser .AbstractQueryCreator ;
68
72
import org .springframework .data .repository .query .parser .Part ;
69
73
import org .springframework .data .repository .query .parser .PartTree ;
82
86
final class CypherQueryCreator extends AbstractQueryCreator <QueryFragmentsAndParameters , Condition > {
83
87
84
88
private final Neo4jMappingContext mappingContext ;
89
+ private final QueryMethod queryMethod ;
85
90
86
91
private final Class <?> domainType ;
87
92
private final NodeDescription <?> nodeDescription ;
@@ -99,6 +104,8 @@ final class CypherQueryCreator extends AbstractQueryCreator<QueryFragmentsAndPar
99
104
100
105
private final Pageable pagingParameter ;
101
106
107
+ private final ScrollPosition scrollPosition ;
108
+
102
109
/**
103
110
* Stores the number of max results, if the {@link PartTree tree} is limiting.
104
111
*/
@@ -113,18 +120,21 @@ final class CypherQueryCreator extends AbstractQueryCreator<QueryFragmentsAndPar
113
120
114
121
private final List <PropertyPathWrapper > propertyPathWrappers ;
115
122
123
+ private final boolean keysetRequiresSort ;
124
+
116
125
/**
117
126
* Can be used to modify the limit of a paged or sliced query.
118
127
*/
119
128
private final UnaryOperator <Integer > limitModifier ;
120
129
121
- CypherQueryCreator (Neo4jMappingContext mappingContext , Class <?> domainType , Neo4jQueryType queryType , PartTree tree ,
130
+ CypherQueryCreator (Neo4jMappingContext mappingContext , QueryMethod queryMethod , Class <?> domainType , Neo4jQueryType queryType , PartTree tree ,
122
131
Neo4jParameterAccessor actualParameters , Collection <PropertyFilter .ProjectedPath > includedProperties ,
123
132
BiFunction <Object , Neo4jPersistentPropertyConverter <?>, Object > parameterConversion ,
124
133
UnaryOperator <Integer > limitModifier ) {
125
134
126
135
super (tree , actualParameters );
127
136
this .mappingContext = mappingContext ;
137
+ this .queryMethod = queryMethod ;
128
138
129
139
this .domainType = domainType ;
130
140
this .nodeDescription = this .mappingContext .getRequiredNodeDescription (this .domainType );
@@ -139,6 +149,7 @@ final class CypherQueryCreator extends AbstractQueryCreator<QueryFragmentsAndPar
139
149
this .parameterConversion = parameterConversion ;
140
150
141
151
this .pagingParameter = actualParameters .getPageable ();
152
+ this .scrollPosition = actualParameters .getScrollPosition ();
142
153
this .limitModifier = limitModifier ;
143
154
144
155
AtomicInteger symbolicNameIndex = new AtomicInteger ();
@@ -148,6 +159,7 @@ final class CypherQueryCreator extends AbstractQueryCreator<QueryFragmentsAndPar
148
159
mappingContext .getPersistentPropertyPath (part .getProperty ())))
149
160
.collect (Collectors .toList ());
150
161
162
+ this .keysetRequiresSort = queryMethod .isScrollQuery () && actualParameters .getScrollPosition () instanceof KeysetScrollPosition ;
151
163
}
152
164
153
165
private class PropertyPathWrapper {
@@ -260,7 +272,12 @@ protected QueryFragmentsAndParameters complete(@Nullable Condition condition, So
260
272
.collect (Collectors .toMap (p -> p .nameOrIndex , p -> parameterConversion .apply (p .value , p .conversionOverride )));
261
273
262
274
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 );
264
281
}
265
282
266
283
@ NonNull
@@ -280,15 +297,12 @@ private QueryFragments createQueryFragments(@Nullable Condition condition, Sort
280
297
}
281
298
}
282
299
283
- // closing action: add the condition and path match
284
- queryFragments .setCondition (conditionFragment );
285
-
286
300
if (!relationshipChain .isEmpty ()) {
287
301
queryFragments .setMatchOn (relationshipChain );
288
302
} else {
289
303
queryFragments .addMatchOn (startNode );
290
304
}
291
- /// end of initial filter query creation
305
+ // end of initial filter query creation
292
306
293
307
if (queryType == Neo4jQueryType .COUNT ) {
294
308
queryFragments .setReturnExpression (Functions .count (Cypher .asterisk ()), true );
@@ -298,20 +312,38 @@ private QueryFragments createQueryFragments(@Nullable Condition condition, Sort
298
312
queryFragments .setDeleteExpression (Constants .NAME_OF_TYPED_ROOT_NODE .apply (nodeDescription ));
299
313
queryFragments .setReturnExpression (Functions .count (Constants .NAME_OF_TYPED_ROOT_NODE .apply (nodeDescription )), true );
300
314
} 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
+
301
337
queryFragments .setReturnBasedOn (nodeDescription , includedProperties , isDistinct );
302
338
queryFragments .setOrderBy (Stream
303
339
.concat (sortItems .stream (),
304
- pagingParameter . getSort (). and ( sort ) .stream ().map (CypherAdapterUtils .sortAdapterFor (nodeDescription )))
340
+ theSort .stream ().map (CypherAdapterUtils .sortAdapterFor (nodeDescription )))
305
341
.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
- }
314
342
}
343
+
344
+ // closing action: add the condition and path match
345
+ queryFragments .setCondition (conditionFragment );
346
+
315
347
return queryFragments ;
316
348
}
317
349
0 commit comments