2525import java .util .ArrayList ;
2626import java .util .Arrays ;
2727import java .util .Collection ;
28+ import java .util .Collections ;
2829import java .util .HashSet ;
2930import java .util .List ;
3031import java .util .Set ;
3132import java .util .function .Predicate ;
3233import java .util .function .UnaryOperator ;
34+ import java .util .stream .Collectors ;
3335
3436import org .apiguardian .api .API ;
3537import org .neo4j .cypherdsl .core .Condition ;
3638import org .neo4j .cypherdsl .core .Conditions ;
3739import org .neo4j .cypherdsl .core .Cypher ;
3840import org .neo4j .cypherdsl .core .Expression ;
41+ import org .neo4j .cypherdsl .core .FunctionInvocation ;
3942import org .neo4j .cypherdsl .core .Functions ;
43+ import org .neo4j .cypherdsl .core .ListComprehension ;
4044import org .neo4j .cypherdsl .core .MapProjection ;
4145import org .neo4j .cypherdsl .core .NamedPath ;
4246import org .neo4j .cypherdsl .core .Node ;
@@ -100,7 +104,13 @@ public StatementBuilder.OrderableOngoingReadingAndWith prepareMatchOf(NodeDescri
100104 * @return An ongoing match
101105 */
102106 public StatementBuilder .OrderableOngoingReadingAndWith prepareMatchOf (NodeDescription <?> nodeDescription ,
103- @ Nullable Condition condition ) {
107+ @ Nullable Condition condition ) {
108+
109+ return prepareMatchOf (nodeDescription , condition , Collections .emptyList ());
110+ }
111+
112+ public StatementBuilder .OrderableOngoingReadingAndWith prepareMatchOf (NodeDescription <?> nodeDescription ,
113+ @ Nullable Condition condition , List <String > includedProperties ) {
104114
105115 String primaryLabel = nodeDescription .getPrimaryLabel ();
106116 List <String > additionalLabels = nodeDescription .getAdditionalLabels ();
@@ -111,7 +121,99 @@ public StatementBuilder.OrderableOngoingReadingAndWith prepareMatchOf(NodeDescri
111121 expressions .add (Constants .NAME_OF_ROOT_NODE );
112122 expressions .add (Functions .id (rootNode ).as (Constants .NAME_OF_INTERNAL_ID ));
113123
114- return match (rootNode ).where (conditionOrNoCondition (condition )).with (expressions .toArray (new Expression [] {}));
124+ if (nodeDescription .containsPossibleCircles (includedProperties )) {
125+ return createPathMatchWithCondition (nodeDescription , includedProperties , condition , rootNode );
126+ } else {
127+ return match (rootNode ).where (conditionOrNoCondition (condition )).with (expressions .toArray (new Expression [] {}));
128+ }
129+ }
130+
131+ private StatementBuilder .OrderableOngoingReadingAndWithWithoutWhere createPathMatchWithCondition (
132+ NodeDescription <?> nodeDescription , List <String > includedProperties , @ Nullable Condition condition , Node rootNode ) {
133+
134+ return createPathMatchWithCondition (null , nodeDescription , includedProperties , condition , rootNode );
135+ }
136+
137+ public StatementBuilder .OrderableOngoingReadingAndWithWithoutWhere createPathMatchWithCondition (
138+ @ Nullable StatementBuilder .OngoingReadingWithoutWhere previousMatches ,
139+ NodeDescription <?> nodeDescription , List <String > includedProperties , @ Nullable Condition condition , Node rootNode ) {
140+
141+ List <Expression > expressions1 = new ArrayList <>();
142+ List <Expression > expressions2 = new ArrayList <>();
143+
144+ String aliasedPathName = "pathPattern" ;
145+ Predicate <String > includeField = s -> includedProperties .isEmpty () || includedProperties .contains (s );
146+ Collection <RelationshipDescription > relationships = getRelationshipDescriptionsUpAndDown (nodeDescription , includeField );
147+ RelationshipPattern patternPath = createRelationships (rootNode , relationships );
148+ NamedPath path = Cypher .path ("p" ).definedBy (patternPath );
149+
150+ // nested nodes flatMap: reduce(...reduce(...))
151+ SymbolicName outerNodesAccumulator = Cypher .name ("a" );
152+ SymbolicName outerNodesVariable = Cypher .name ("b" );
153+ SymbolicName innerNodesAccumulator = Cypher .name ("c" );
154+ SymbolicName innerNodesVariable = Cypher .name ("d" );
155+ SymbolicName innerNodesListIterator = Cypher .name ("e" );
156+ ListComprehension innerNodesListComprehension = Cypher .listWith (innerNodesListIterator )
157+ .in (Cypher .name (aliasedPathName )).returning (Functions .nodes (innerNodesListIterator ));
158+
159+ FunctionInvocation innerNodesReduce = createInnerReduce (innerNodesAccumulator , innerNodesVariable ,
160+ innerNodesListComprehension );
161+
162+ FunctionInvocation outerNodesReduce = createOuterReduce (outerNodesAccumulator , outerNodesVariable ,
163+ innerNodesReduce );
164+
165+ // nested relationships flatMap: reduce(...reduce(...))
166+ SymbolicName outerRelationshipsAccumulator = Cypher .name ("f" );
167+ SymbolicName outerRelationshipsVariable = Cypher .name ("g" );
168+ SymbolicName innerRelationshipsAccumulator = Cypher .name ("h" );
169+ SymbolicName innerRelationshipsVariable = Cypher .name ("i" );
170+ SymbolicName innerRelationshipsListIterator = Cypher .name ("j" );
171+ ListComprehension innerRelationshipsListComprehension = Cypher .listWith (innerRelationshipsListIterator )
172+ .in (Cypher .name (aliasedPathName )).returning (Functions .relationships (innerRelationshipsListIterator ));
173+
174+ FunctionInvocation innerRelationshipReduce = createInnerReduce (innerRelationshipsAccumulator ,
175+ innerRelationshipsVariable , innerRelationshipsListComprehension );
176+
177+ FunctionInvocation outerRelationshipsReduce = createOuterReduce (outerRelationshipsAccumulator ,
178+ outerRelationshipsVariable , innerRelationshipReduce );
179+
180+ // WITH n, collect(p) as pathPattern
181+ expressions1 .add (Constants .NAME_OF_ROOT_NODE );
182+ expressions1 .add (Functions .collect (path ).as (aliasedPathName ));
183+ // WITH n, reduce(nodes) as __sm__, reduce(relationships) as __sr__
184+ expressions2 .add (Constants .NAME_OF_ROOT_NODE );
185+ expressions2 .add (outerNodesReduce .as (Constants .NAME_OF_SYNTHESIZED_RELATED_NODES ));
186+ expressions2 .add (outerRelationshipsReduce .as (Constants .NAME_OF_SYNTHESIZED_RELATIONS ));
187+
188+ StatementBuilder .OngoingReadingWithoutWhere match = match (path );
189+
190+ if (previousMatches != null ) {
191+ match = previousMatches .match (path );
192+ }
193+
194+ return match
195+ .where (conditionOrNoCondition (condition ))
196+ .with (expressions1 .toArray (new Expression []{}))
197+ .with (expressions2 .toArray (new Expression []{}));
198+ }
199+
200+ private FunctionInvocation createOuterReduce (SymbolicName outerNodesAccumulator , SymbolicName outerNodesVariable , FunctionInvocation innerNodesReduce ) {
201+ return Functions .reduce (outerNodesVariable )
202+ .in (innerNodesReduce )
203+ .map (Cypher .caseExpression ()
204+ .when (outerNodesVariable .in (outerNodesAccumulator ))
205+ .then (outerNodesAccumulator )
206+ .elseDefault (outerNodesAccumulator .add (outerNodesVariable )))
207+ .accumulateOn (outerNodesAccumulator )
208+ .withInitialValueOf (Cypher .listOf ());
209+ }
210+
211+ private FunctionInvocation createInnerReduce (SymbolicName innerNodesAccumulator , SymbolicName innerNodesVariable , ListComprehension innerNodesListComprehension ) {
212+ return Functions .reduce (innerNodesVariable )
213+ .in (innerNodesListComprehension )
214+ .map (innerNodesAccumulator .add (innerNodesVariable ))
215+ .accumulateOn (innerNodesAccumulator )
216+ .withInitialValueOf (Cypher .listOf ());
115217 }
116218
117219 /**
@@ -332,8 +434,8 @@ public Statement prepareDeleteOf(
332434 .build ();
333435 }
334436
335- public Expression createReturnStatementForMatch (NodeDescription <?> nodeDescription ) {
336- return createReturnStatementForMatch (nodeDescription , null );
437+ public Expression [] createReturnStatementForMatch (NodeDescription <?> nodeDescription ) {
438+ return createReturnStatementForMatch (nodeDescription , Collections . emptyList () );
337439 }
338440
339441 /**
@@ -372,20 +474,25 @@ public Expression createReturnStatementForMatch(NodeDescription<?> nodeDescripti
372474
373475 /**
374476 * @param nodeDescription Description of the root node
375- * @param inputProperties A list of Java properties of the domain to be included. Those properties are compared with
477+ * @param includedProperties A list of Java properties of the domain to be included. Those properties are compared with
376478 * the field names of graph properties respectively relationships.
377479 * @return An expresion to be returned by a Cypher statement
378480 */
379- public Expression createReturnStatementForMatch (NodeDescription <?> nodeDescription ,
380- @ Nullable List <String > inputProperties ) {
481+ public Expression [] createReturnStatementForMatch (NodeDescription <?> nodeDescription ,
482+ List <String > includedProperties ) {
381483
382- Predicate <String > includeField = s -> inputProperties == null || inputProperties .isEmpty ()
383- || inputProperties .contains (s );
384-
385- SymbolicName nodeName = Constants .NAME_OF_ROOT_NODE ;
386484 List <RelationshipDescription > processedRelationships = new ArrayList <>();
387-
388- return projectPropertiesAndRelationships (nodeDescription , nodeName , includeField , processedRelationships );
485+ if (nodeDescription .containsPossibleCircles (includedProperties )) {
486+ List <Expression > returnExpressions = new ArrayList <>();
487+ Node rootNode = anyNode (Constants .NAME_OF_ROOT_NODE );
488+ returnExpressions .add (rootNode .as (Constants .NAME_OF_SYNTHESIZED_ROOT_NODE ));
489+ returnExpressions .add (Cypher .name (Constants .NAME_OF_SYNTHESIZED_RELATED_NODES ));
490+ returnExpressions .add (Cypher .name (Constants .NAME_OF_SYNTHESIZED_RELATIONS ));
491+ return returnExpressions .toArray (new Expression []{});
492+ } else {
493+ Predicate <String > includeField = s -> includedProperties .isEmpty () || includedProperties .contains (s );
494+ return new Expression []{projectPropertiesAndRelationships (nodeDescription , Constants .NAME_OF_ROOT_NODE , includeField , processedRelationships )};
495+ }
389496 }
390497
391498 // recursive entry point for relationships in return statement
@@ -398,30 +505,23 @@ private MapProjection projectAllPropertiesAndRelationships(NodeDescription<?> no
398505 }
399506
400507 private MapProjection projectPropertiesAndRelationships (NodeDescription <?> nodeDescription , SymbolicName nodeName ,
401- Predicate <String > includeProperty , List <RelationshipDescription > processedRelationships ) {
508+ Predicate <String > includedProperties , List <RelationshipDescription > processedRelationships ) {
402509
403- List <Object > propertiesProjection = projectNodeProperties (nodeDescription , nodeName , includeProperty );
510+ List <Object > propertiesProjection = projectNodeProperties (nodeDescription , nodeName , includedProperties );
404511 List <Object > contentOfProjection = new ArrayList <>(propertiesProjection );
405512
406- Collection <RelationshipDescription > relationships = getRelationshipDescriptionsUpAndDown (nodeDescription );
407- relationships .removeIf (r -> !includeProperty .test (r .getFieldName ()));
513+ Collection <RelationshipDescription > relationships = getRelationshipDescriptionsUpAndDown (nodeDescription , includedProperties );
514+ relationships .removeIf (r -> !includedProperties .test (r .getFieldName ()));
408515
409- if (nodeDescription .containsPossibleCircles ()) {
410- Node node = anyNode (nodeName );
411- RelationshipPattern pattern = createRelationships (node , relationships );
412- NamedPath p = Cypher .path ("p" ).definedBy (pattern );
413- contentOfProjection .add (Constants .NAME_OF_PATHS );
414- contentOfProjection .add (Cypher .listBasedOn (p ).returning (p ));
415- } else {
416- contentOfProjection .addAll (generateListsFor (relationships , nodeName , processedRelationships ));
417- }
516+ contentOfProjection .addAll (generateListsFor (relationships , nodeName , processedRelationships ));
418517 return Cypher .anyNode (nodeName ).project (contentOfProjection );
419518 }
420519
421520 @ NonNull
422- static Collection <RelationshipDescription > getRelationshipDescriptionsUpAndDown (NodeDescription <?> nodeDescription ) {
423- Collection < RelationshipDescription > relationships = new HashSet <>( nodeDescription . getRelationships ());
521+ static Collection <RelationshipDescription > getRelationshipDescriptionsUpAndDown (NodeDescription <?> nodeDescription ,
522+ Predicate < String > includedProperties ) {
424523
524+ Collection <RelationshipDescription > relationships = new HashSet <>(nodeDescription .getRelationships ());
425525 for (NodeDescription <?> childDescription : nodeDescription .getChildNodeDescriptionsInHierarchy ()) {
426526 childDescription .getRelationships ().forEach (concreteRelationship -> {
427527
@@ -432,19 +532,25 @@ static Collection<RelationshipDescription> getRelationshipDescriptionsUpAndDown(
432532 }
433533 });
434534 }
435- return relationships ;
535+
536+ return relationships .stream ().filter (relationshipDescription ->
537+ includedProperties .test (relationshipDescription .getFieldName ()))
538+ .collect (Collectors .toSet ());
436539 }
437540
438541 private RelationshipPattern createRelationships (Node node , Collection <RelationshipDescription > relationshipDescriptions ) {
439542 RelationshipPattern relationship ;
440543
441544 Direction determinedDirection = determineDirection (relationshipDescriptions );
442545 if (Direction .OUTGOING .equals (determinedDirection )) {
443- relationship = node .relationshipTo (anyNode (), collectFirstLevelRelationshipTypes (relationshipDescriptions ));
546+ relationship = node .relationshipTo (anyNode (), collectFirstLevelRelationshipTypes (relationshipDescriptions ))
547+ .min (0 ).max (1 );
444548 } else if (Direction .INCOMING .equals (determinedDirection )) {
445- relationship = node .relationshipFrom (anyNode (), collectFirstLevelRelationshipTypes (relationshipDescriptions ));
549+ relationship = node .relationshipFrom (anyNode (), collectFirstLevelRelationshipTypes (relationshipDescriptions ))
550+ .min (0 ).max (1 );
446551 } else {
447- relationship = node .relationshipBetween (anyNode (), collectFirstLevelRelationshipTypes (relationshipDescriptions ));
552+ relationship = node .relationshipBetween (anyNode (), collectFirstLevelRelationshipTypes (relationshipDescriptions ))
553+ .min (0 ).max (1 );
448554 }
449555
450556 Set <RelationshipDescription > processedRelationshipDescriptions = new HashSet <>(relationshipDescriptions );
0 commit comments