From 8a3f79c11724e25b664a73e91d9e6e5c0c456085 Mon Sep 17 00:00:00 2001 From: Niels Date: Tue, 25 Jul 2023 22:22:57 +0200 Subject: [PATCH] GH-952: Fix entity filtering when mapping query results. --- .../org/neo4j/ogm/context/EntityFilter.java | 69 ++++++++ .../neo4j/ogm/context/GraphEntityMapper.java | 17 +- .../ogm/context/GraphRowListModelMapper.java | 8 +- .../ogm/context/GraphRowModelMapper.java | 19 +-- .../java/org/neo4j/ogm/domain/gh952/Book.java | 48 ++++++ .../neo4j/ogm/domain/gh952/BookWasReadBy.java | 62 ++++++++ .../org/neo4j/ogm/domain/gh952/Human.java | 50 ++++++ .../ogm/domain/gh952/HumanIsParentOf.java | 62 ++++++++ .../domain/gh952/UuidGenerationStrategy.java | 14 ++ .../capability/QueryCapabilityGH952Test.java | 147 ++++++++++++++++++ 10 files changed, 468 insertions(+), 28 deletions(-) create mode 100644 core/src/main/java/org/neo4j/ogm/context/EntityFilter.java create mode 100644 neo4j-ogm-tests/neo4j-ogm-integration-tests/src/test/java/org/neo4j/ogm/domain/gh952/Book.java create mode 100644 neo4j-ogm-tests/neo4j-ogm-integration-tests/src/test/java/org/neo4j/ogm/domain/gh952/BookWasReadBy.java create mode 100644 neo4j-ogm-tests/neo4j-ogm-integration-tests/src/test/java/org/neo4j/ogm/domain/gh952/Human.java create mode 100644 neo4j-ogm-tests/neo4j-ogm-integration-tests/src/test/java/org/neo4j/ogm/domain/gh952/HumanIsParentOf.java create mode 100644 neo4j-ogm-tests/neo4j-ogm-integration-tests/src/test/java/org/neo4j/ogm/domain/gh952/UuidGenerationStrategy.java create mode 100644 neo4j-ogm-tests/neo4j-ogm-integration-tests/src/test/java/org/neo4j/ogm/persistence/session/capability/QueryCapabilityGH952Test.java diff --git a/core/src/main/java/org/neo4j/ogm/context/EntityFilter.java b/core/src/main/java/org/neo4j/ogm/context/EntityFilter.java new file mode 100644 index 000000000..e1c31a111 --- /dev/null +++ b/core/src/main/java/org/neo4j/ogm/context/EntityFilter.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2002-2023 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.neo4j.ogm.context; + +import java.util.Optional; + +import org.neo4j.ogm.model.GraphModel; +import org.neo4j.ogm.model.Node; +import org.neo4j.ogm.response.model.DefaultGraphModel; +import org.neo4j.ogm.response.model.NodeModel; + +/** + * Filter for entities to check whether nodes/relationships should be included in the mapping result. + * + * @author Niels Oertel + */ +interface EntityFilter { + + /** + * Include any entity. + */ + EntityFilter INCLUDE_ALWAYS = (graphModel, nativeId, isNode) -> true; + + /** + * Include all relationships but only nodes which are not generated. + */ + EntityFilter WITHOUT_GENERATED_NODES = (graphModel, nativeId, isNode) -> { + if (!isNode) { + return true; + } else { + Optional node = ((DefaultGraphModel) graphModel).findNode(nativeId); + if (!node.isPresent()) { + return true; // this should actually never happen but to keep existing behaviour, we are not throwing an exception + } + return node.map(n -> !((NodeModel) n).isGeneratedNode()).get(); + } + }; + + /** + * Check if an object with given native id should be included in the mapping result. + * + * @param graphModel + * The graph model. + * @param nativeObjectId + * The object's native id. + * @param isNode + * True if the object is a node, false if relationship. + * + * @return True if the object should be included. + */ + boolean shouldIncludeModelObject(GraphModel graphModel, long nativeObjectId, boolean isNode); + +} diff --git a/core/src/main/java/org/neo4j/ogm/context/GraphEntityMapper.java b/core/src/main/java/org/neo4j/ogm/context/GraphEntityMapper.java index d83593977..07f19d9e3 100644 --- a/core/src/main/java/org/neo4j/ogm/context/GraphEntityMapper.java +++ b/core/src/main/java/org/neo4j/ogm/context/GraphEntityMapper.java @@ -24,7 +24,6 @@ import java.lang.reflect.InvocationTargetException; import java.util.*; -import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Predicate; @@ -54,6 +53,7 @@ * @author Vince Bickers * @author Luanne Misquitta * @author Michael J. Simons + * @author Niels Oertel */ public class GraphEntityMapper { @@ -81,7 +81,7 @@ public GraphEntityMapper(MetaData metaData, MappingContext mappingContext, Entit } List map(Class type, Response listOfGraphModels) { - return map(type, listOfGraphModels, (m, n) -> true); + return map(type, listOfGraphModels, EntityFilter.INCLUDE_ALWAYS); } /** @@ -92,7 +92,7 @@ List map(Class type, Response listOfGraphModels) { * @return The list of entities represented by the list of graph models. */ List map(Class type, Response graphModelResponse, - BiFunction additionalNodeFilter) { + EntityFilter additionalEntityFilter) { // Those are the ids of all mapped nodes. Set mappedNodeIds = new LinkedHashSet<>(); @@ -103,7 +103,7 @@ List map(Class type, Response graphModelResponse, // Execute mapping for each individual model Consumer mapContentOfIndividualModel = - graphModel -> mapContentOf(graphModel, additionalNodeFilter, returnedNodeIds, mappedRelationshipIds, + graphModel -> mapContentOf(graphModel, additionalEntityFilter, returnedNodeIds, mappedRelationshipIds, returnedRelationshipIds, mappedNodeIds); GraphModel graphModel = null; @@ -139,20 +139,21 @@ List map(Class type, Response graphModelResponse, private void mapContentOf( GraphModel graphModel, - BiFunction additionalNodeFilter, + EntityFilter additionalEntityFilter, Set returnedNodeIds, Set mappedRelationshipIds, Set returnedRelationshipIds, Set mappedNodeIds ) { - Predicate includeInResult = id -> additionalNodeFilter.apply(graphModel, id); + Predicate includeNodeInResult = id -> additionalEntityFilter.shouldIncludeModelObject(graphModel, id, true); + Predicate includeRelInResult = id -> additionalEntityFilter.shouldIncludeModelObject(graphModel, id, false); try { Set newNodeIds = mapNodes(graphModel); - returnedNodeIds.addAll(newNodeIds.stream().filter(includeInResult).collect(toList())); + returnedNodeIds.addAll(newNodeIds.stream().filter(includeNodeInResult).collect(toList())); mappedNodeIds.addAll(newNodeIds); newNodeIds = mapRelationships(graphModel); - returnedRelationshipIds.addAll(newNodeIds.stream().filter(includeInResult).collect(toList())); + returnedRelationshipIds.addAll(newNodeIds.stream().filter(includeRelInResult).collect(toList())); mappedRelationshipIds.addAll(newNodeIds); } catch (MappingException e) { throw e; diff --git a/core/src/main/java/org/neo4j/ogm/context/GraphRowListModelMapper.java b/core/src/main/java/org/neo4j/ogm/context/GraphRowListModelMapper.java index fe01c1190..2b0d3801a 100644 --- a/core/src/main/java/org/neo4j/ogm/context/GraphRowListModelMapper.java +++ b/core/src/main/java/org/neo4j/ogm/context/GraphRowListModelMapper.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2022 "Neo4j," + * Copyright (c) 2002-2023 "Neo4j," * Neo4j Sweden AB [http://neo4j.com] * * This file is part of Neo4j. @@ -22,7 +22,6 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Set; -import java.util.function.BiFunction; import org.neo4j.ogm.metadata.MetaData; import org.neo4j.ogm.model.GraphModel; @@ -36,6 +35,7 @@ /** * @author Vince Bickers * @author Michael J. Simons + * @author Niels Oertel */ public class GraphRowListModelMapper implements ResponseMapper { @@ -96,8 +96,8 @@ public String[] columns() { }; // although it looks like that the `idsOfResultEntities` will stay empty, they won't, trust us. - BiFunction includeModelObject = - (graphModel, nativeId) -> idsOfResultEntities.contains(nativeId); + EntityFilter includeModelObject = + (graphModel, nativeId, isNode) -> idsOfResultEntities.contains(nativeId); return delegate.map(type, graphResponse, includeModelObject); } diff --git a/core/src/main/java/org/neo4j/ogm/context/GraphRowModelMapper.java b/core/src/main/java/org/neo4j/ogm/context/GraphRowModelMapper.java index 7a74fb65e..d655f5e03 100644 --- a/core/src/main/java/org/neo4j/ogm/context/GraphRowModelMapper.java +++ b/core/src/main/java/org/neo4j/ogm/context/GraphRowModelMapper.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2022 "Neo4j," + * Copyright (c) 2002-2023 "Neo4j," * Neo4j Sweden AB [http://neo4j.com] * * This file is part of Neo4j. @@ -18,19 +18,14 @@ */ package org.neo4j.ogm.context; -import java.util.Optional; -import java.util.function.BiFunction; - import org.neo4j.ogm.metadata.MetaData; import org.neo4j.ogm.model.GraphModel; -import org.neo4j.ogm.model.Node; import org.neo4j.ogm.response.Response; -import org.neo4j.ogm.response.model.DefaultGraphModel; -import org.neo4j.ogm.response.model.NodeModel; import org.neo4j.ogm.session.EntityInstantiator; /** * @author Michael J. Simons + * @author Niels Oertel */ public class GraphRowModelMapper implements ResponseMapper { @@ -44,14 +39,6 @@ public GraphRowModelMapper(MetaData metaData, MappingContext mappingContext, @Override public Iterable map(Class type, Response response) { - - BiFunction isNotGeneratedNode = (graphModel, nativeId) -> { - Optional node = ((DefaultGraphModel) graphModel).findNode(nativeId); - if (!node.isPresent()) { - return true; // Native id describes a relationship - } - return node.map(n -> !((NodeModel) n).isGeneratedNode()).get(); - }; - return delegate.map(type, response, isNotGeneratedNode); + return delegate.map(type, response, EntityFilter.WITHOUT_GENERATED_NODES); } } diff --git a/neo4j-ogm-tests/neo4j-ogm-integration-tests/src/test/java/org/neo4j/ogm/domain/gh952/Book.java b/neo4j-ogm-tests/neo4j-ogm-integration-tests/src/test/java/org/neo4j/ogm/domain/gh952/Book.java new file mode 100644 index 000000000..7e6209dd3 --- /dev/null +++ b/neo4j-ogm-tests/neo4j-ogm-integration-tests/src/test/java/org/neo4j/ogm/domain/gh952/Book.java @@ -0,0 +1,48 @@ +package org.neo4j.ogm.domain.gh952; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.neo4j.ogm.annotation.GeneratedValue; +import org.neo4j.ogm.annotation.Id; +import org.neo4j.ogm.annotation.NodeEntity; + +@NodeEntity(Book.LABEL) +public class Book { + + public static final String LABEL = "Book"; + + @Id + @GeneratedValue(strategy = UuidGenerationStrategy.class) + private String uuid; + + private String title; + + private List readBy = List.of(); + + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public List getReadBy() { + return Collections.unmodifiableList(readBy); + } + + public void setReadBy(List readBy) { + this.readBy = new ArrayList<>(readBy); + } + +} diff --git a/neo4j-ogm-tests/neo4j-ogm-integration-tests/src/test/java/org/neo4j/ogm/domain/gh952/BookWasReadBy.java b/neo4j-ogm-tests/neo4j-ogm-integration-tests/src/test/java/org/neo4j/ogm/domain/gh952/BookWasReadBy.java new file mode 100644 index 000000000..bb07adeb3 --- /dev/null +++ b/neo4j-ogm-tests/neo4j-ogm-integration-tests/src/test/java/org/neo4j/ogm/domain/gh952/BookWasReadBy.java @@ -0,0 +1,62 @@ +package org.neo4j.ogm.domain.gh952; + +import java.time.Instant; + +import org.neo4j.ogm.annotation.EndNode; +import org.neo4j.ogm.annotation.GeneratedValue; +import org.neo4j.ogm.annotation.Id; +import org.neo4j.ogm.annotation.RelationshipEntity; +import org.neo4j.ogm.annotation.StartNode; +import org.neo4j.ogm.annotation.typeconversion.DateLong; + +@RelationshipEntity("READ_BY") +public class BookWasReadBy { + + public static final String TYPE = "READ_BY"; + + @Id + @GeneratedValue(strategy = UuidGenerationStrategy.class) + private String uuid; + + @DateLong + private Instant date; + + @StartNode + private Book book; + + @EndNode + private Human human; + + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + public Instant getDate() { + return date; + } + + public void setDate(Instant date) { + this.date = date; + } + + public Book getBook() { + return book; + } + + public void setBook(Book book) { + this.book = book; + } + + public Human getHuman() { + return human; + } + + public void setHuman(Human human) { + this.human = human; + } + +} diff --git a/neo4j-ogm-tests/neo4j-ogm-integration-tests/src/test/java/org/neo4j/ogm/domain/gh952/Human.java b/neo4j-ogm-tests/neo4j-ogm-integration-tests/src/test/java/org/neo4j/ogm/domain/gh952/Human.java new file mode 100644 index 000000000..4837e1557 --- /dev/null +++ b/neo4j-ogm-tests/neo4j-ogm-integration-tests/src/test/java/org/neo4j/ogm/domain/gh952/Human.java @@ -0,0 +1,50 @@ +package org.neo4j.ogm.domain.gh952; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.neo4j.ogm.annotation.GeneratedValue; +import org.neo4j.ogm.annotation.Id; +import org.neo4j.ogm.annotation.NodeEntity; +import org.neo4j.ogm.annotation.Relationship; + +@NodeEntity(Human.LABEL) +public class Human { + + public static final String LABEL = "Human"; + + @Id + @GeneratedValue(strategy = UuidGenerationStrategy.class) + private String uuid; + + private String name; + + @Relationship(type = HumanIsParentOf.TYPE, direction = Relationship.OUTGOING) + private List children; + + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getChildren() { + return Collections.unmodifiableList(children); + } + + public void setChildren(List children) { + this.children = new ArrayList<>(children); + } + +} diff --git a/neo4j-ogm-tests/neo4j-ogm-integration-tests/src/test/java/org/neo4j/ogm/domain/gh952/HumanIsParentOf.java b/neo4j-ogm-tests/neo4j-ogm-integration-tests/src/test/java/org/neo4j/ogm/domain/gh952/HumanIsParentOf.java new file mode 100644 index 000000000..c5872c492 --- /dev/null +++ b/neo4j-ogm-tests/neo4j-ogm-integration-tests/src/test/java/org/neo4j/ogm/domain/gh952/HumanIsParentOf.java @@ -0,0 +1,62 @@ +package org.neo4j.ogm.domain.gh952; + +import java.time.Instant; + +import org.neo4j.ogm.annotation.EndNode; +import org.neo4j.ogm.annotation.GeneratedValue; +import org.neo4j.ogm.annotation.Id; +import org.neo4j.ogm.annotation.RelationshipEntity; +import org.neo4j.ogm.annotation.StartNode; +import org.neo4j.ogm.annotation.typeconversion.DateLong; + +@RelationshipEntity(HumanIsParentOf.TYPE) +public class HumanIsParentOf { + + public static final String TYPE = "PARENT_OF"; + + @Id + @GeneratedValue(strategy = UuidGenerationStrategy.class) + private String uuid; + + @StartNode + private Human parent; + + @EndNode + private Human child; + + @DateLong + private Instant lastMeeting; + + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + public Human getParent() { + return parent; + } + + public void setParent(Human parent) { + this.parent = parent; + } + + public Human getChild() { + return child; + } + + public void setChild(Human child) { + this.child = child; + } + + public Instant getLastMeeting() { + return lastMeeting; + } + + public void setLastMeeting(Instant lastMeeting) { + this.lastMeeting = lastMeeting; + } + +} diff --git a/neo4j-ogm-tests/neo4j-ogm-integration-tests/src/test/java/org/neo4j/ogm/domain/gh952/UuidGenerationStrategy.java b/neo4j-ogm-tests/neo4j-ogm-integration-tests/src/test/java/org/neo4j/ogm/domain/gh952/UuidGenerationStrategy.java new file mode 100644 index 000000000..eb9b53284 --- /dev/null +++ b/neo4j-ogm-tests/neo4j-ogm-integration-tests/src/test/java/org/neo4j/ogm/domain/gh952/UuidGenerationStrategy.java @@ -0,0 +1,14 @@ +package org.neo4j.ogm.domain.gh952; + +import java.util.UUID; + +import org.neo4j.ogm.id.IdStrategy; + +public class UuidGenerationStrategy implements IdStrategy { + + @Override + public Object generateId(Object entity) { + return UUID.randomUUID().toString(); + } + +} diff --git a/neo4j-ogm-tests/neo4j-ogm-integration-tests/src/test/java/org/neo4j/ogm/persistence/session/capability/QueryCapabilityGH952Test.java b/neo4j-ogm-tests/neo4j-ogm-integration-tests/src/test/java/org/neo4j/ogm/persistence/session/capability/QueryCapabilityGH952Test.java new file mode 100644 index 000000000..0b3f8deed --- /dev/null +++ b/neo4j-ogm-tests/neo4j-ogm-integration-tests/src/test/java/org/neo4j/ogm/persistence/session/capability/QueryCapabilityGH952Test.java @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2002-2023 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.neo4j.ogm.persistence.session.capability; + +import java.io.IOException; +import java.time.Instant; +import java.util.Map; + +import org.assertj.core.api.BDDAssertions; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.neo4j.ogm.domain.gh952.BookWasReadBy; +import org.neo4j.ogm.session.Neo4jSession; +import org.neo4j.ogm.session.Session; +import org.neo4j.ogm.session.SessionFactory; +import org.neo4j.ogm.testutil.LoggerRule; +import org.neo4j.ogm.testutil.TestContainersTestBase; +import org.slf4j.LoggerFactory; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.LoggerContext; + +/** + * This test class is specifically for GH-952 because it requires special preparation magic that should be kept separate + * from other tests. + * + * @author Niels Oertel + */ +public class QueryCapabilityGH952Test extends TestContainersTestBase { + + private SessionFactory sessionFactory; + private Session session; + + static { + LoggerContext logCtx = (LoggerContext) LoggerFactory.getILoggerFactory(); + logCtx.getLogger(Neo4jSession.class).setLevel(Level.DEBUG); + } + + @Rule + public final LoggerRule loggerRule = new LoggerRule(); + + @Before + public void init() throws IOException { + sessionFactory = new SessionFactory(getDriver(), "org.neo4j.ogm.domain.gh952"); + session = sessionFactory.openSession(); + session.purgeDatabase(); + session.clear(); + } + + private long bringRelAndNodeIdsToSameLevel() { + long currentLargestNodeId = session.queryForObject(Long.class, ""// + + "MERGE (n1:Dummy{i:0}) "// + + "MERGE (n2:Dummy{i:1}) "// + + "RETURN id(n2)", Map.of()); + long currentLargestRelId = session.queryForObject(Long.class, ""// + + "MATCH (n1:Dummy{i:0}) "// + + "MATCH (n2:Dummy{i:1}) "// + + "MERGE (n1)-[r1:FOOBAR{i:0}]->(n2) "// + + "RETURN id(r1)", Map.of()); + + if (currentLargestNodeId == currentLargestRelId) { + return currentLargestNodeId + 1L; + } else if (currentLargestNodeId > currentLargestRelId) { + // need to create currentLargestNodeId - currentLargestRelId relationships + long numRelsToCreate = currentLargestNodeId - currentLargestRelId; + for (long i = 0; i < numRelsToCreate; ++i) { + session.queryForObject(Long.class, ""// + + "MATCH (n1:Dummy{i:0}) "// + + "MATCH (n2:Dummy{i:1}) "// + + "MERGE (n1)-[r1:FOOBAR{i:$i}]->(n2) "// + + "RETURN id(r1)", Map.of("i", i + 1L)); + } + return currentLargestNodeId + 1L; + } else { + // need to create currentLargestRelId - currentLargestNodeId nodes + long numNodesToCreate = currentLargestRelId - currentLargestNodeId; + for (long i = 0; i < numNodesToCreate; ++i) { + session.queryForObject(Long.class, ""// + + "MERGE (n1:Dummy{uuid:'A',i:$i}) "// + + "RETURN id(n2)", Map.of("i", i + 2L)); + } + return currentLargestRelId + 1L; + } + } + + @Test // GH-952 + public void shouldCorrectlyMapNodesAndRelsWithSameId() { + // prepare Neo4j by ensuring the next node and rel will get the same IDs + long nextId = bringRelAndNodeIdsToSameLevel(); + + // create test graph + session.query(""// + + "MERGE (h1:Human{name:'Jane Doe', uuid:'AAAA0001'}) "// + + "MERGE (h2:Human{name:'Jon Doe Jr.', uuid:'AAAA0002'}) "// + + "MERGE (b:Book{title:'Moby-Dick', uuid:'AAAA0003'}) "// + + "MERGE (h1)-[:PARENT_OF{uuid:'BBBB0001', lastMeeting:1689347516000}]->(h2) "// + + "MERGE (b)-[:READ_BY{uuid:'BBBB0002', date:1689693116000}]->(h1)", // + Map.of()); + + // verify that we reached the expected setup + BDDAssertions.assertThat(session.queryForObject(Long.class, "MATCH (n{uuid:'AAAA0001'}) RETURN id(n)", Map.of())).isEqualTo(nextId); + BDDAssertions.assertThat(session.queryForObject(Long.class, "MATCH (n{uuid:'AAAA0002'}) RETURN id(n)", Map.of())).isEqualTo(nextId + 1L); + BDDAssertions.assertThat(session.queryForObject(Long.class, "MATCH (n{uuid:'AAAA0003'}) RETURN id(n)", Map.of())).isEqualTo(nextId + 2L); + BDDAssertions.assertThat(session.queryForObject(Long.class, "MATCH ()-[r{uuid:'BBBB0001'}]->() RETURN id(r)", Map.of())).isEqualTo(nextId); + BDDAssertions.assertThat(session.queryForObject(Long.class, "MATCH ()-[r{uuid:'BBBB0002'}]->() RETURN id(r)", Map.of())) + .isEqualTo(nextId + 1L); + + // load the relationship entity between Moby-Dick and Jane Doe including the children of Jane + BDDAssertions.assertThat(// + session.query(BookWasReadBy.class, ""// + + "MATCH (b:Book{title:$bookTitle})-[r:READ_BY]->(h:Human) "// + + "RETURN"// + + " r, b, h, [[ (h)-[p:PARENT_OF]->(c) | [p, c]]], id(r)", // + Map.of("bookTitle", "Moby-Dick")))// + .hasSize(1)// -> this check fails without the fix for GH-952 + .allSatisfy(bookReadBy -> { + BDDAssertions.assertThat(bookReadBy.getBook().getTitle()).isEqualTo("Moby-Dick"); + BDDAssertions.assertThat(bookReadBy.getDate()).isEqualTo(Instant.ofEpochMilli(1689693116000L)); + BDDAssertions.assertThat(bookReadBy.getHuman().getName()).isEqualTo("Jane Doe"); + BDDAssertions.assertThat(bookReadBy.getHuman().getChildren())// + .hasSize(1)// + .allSatisfy(parentOf -> { + BDDAssertions.assertThat(parentOf.getParent().getName()).isEqualTo("Jane Doe"); + BDDAssertions.assertThat(parentOf.getLastMeeting()).isEqualTo(Instant.ofEpochMilli(1689347516000L)); + BDDAssertions.assertThat(parentOf.getChild().getName()).isEqualTo("Jon Doe Jr."); + }); + }); + } + +}