This is an automated email from the ASF dual-hosted git repository.

ntimofeev pushed a commit to branch STABLE-4.2
in repository https://gitbox.apache.org/repos/asf/cayenne.git

commit bbe569a286d2691e2a78592261a6608306ff3077
Author: John Huss <[email protected]>
AuthorDate: Thu Oct 30 17:09:02 2025 +0400

    CAY-2904 Disjoint prefetch returns incorrect data
---
 .../cayenne/access/HierarchicalObjectResolver.java |  35 +++++
 .../access/ResultScanParentAttachmentStrategy.java |   7 +-
 .../ConcurrentLinkedHashMap.java                   |   1 +
 .../access/DataContextPrefetchMultistepIT.java     | 161 +++++++++++++++++++++
 4 files changed, 203 insertions(+), 1 deletion(-)

diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/access/HierarchicalObjectResolver.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/access/HierarchicalObjectResolver.java
index a9f6a0f79..55c4beff2 100644
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/access/HierarchicalObjectResolver.java
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/access/HierarchicalObjectResolver.java
@@ -110,6 +110,20 @@ class HierarchicalObjectResolver {
                 return true;
             }
 
+            PrefetchProcessorNode processorNode = (PrefetchProcessorNode) node;
+            PrefetchProcessorNode parentProcessorNode = 
(PrefetchProcessorNode) processorNode.getParent();
+            
+            // If parent is a joint node, defer processing until the joint 
node is processed
+            if (parentProcessorNode.getSemantics() == 
PrefetchTreeNode.JOINT_PREFETCH_SEMANTICS) {
+                // Mark that we need to process this later, but don't fetch 
data now
+                return true;
+            }
+            
+            return processDisjointByIdNode(node);
+        }
+        
+        // Process a disjointById node without checking for deferral
+        private boolean processDisjointByIdNode(PrefetchTreeNode node) {
             PrefetchProcessorNode processorNode = (PrefetchProcessorNode) node;
             PrefetchProcessorNode parentProcessorNode = 
(PrefetchProcessorNode) processorNode.getParent();
             ObjRelationship relationship = 
processorNode.getIncoming().getRelationship();
@@ -142,6 +156,12 @@ class HierarchicalObjectResolver {
                 parentDataRows = parentProcessorNode.getDataRows();
             }
 
+            // If parent data rows is null or empty, there's nothing to 
prefetch
+            if (parentDataRows == null || parentDataRows.isEmpty()) {
+                processorNode.setDataRows(new ArrayList<>());
+                return true;
+            }
+
             int maxIdQualifierSize = 
context.getParentDataDomain().getMaxIdQualifierSize();
             List<DbJoin> joins = lastDbRelationship.getJoins();
 
@@ -322,6 +342,17 @@ class HierarchicalObjectResolver {
                     processorNode.getResolvedRows(),
                     queryMetadata.isRefreshingObjects());
 
+            // Now process any deferred disjointById children
+            DisjointByIdProcessor byIdProcessor = new DisjointByIdProcessor();
+            for (PrefetchTreeNode child : node.getChildren()) {
+                if (child.isDisjointByIdPrefetch()) {
+                    // Now that the joint parent has been processed, we can 
fetch the disjointById data
+                    byIdProcessor.processDisjointByIdNode(child);
+                    // And resolve the objects
+                    startDisjointPrefetch(child);
+                }
+            }
+
             return true;
         }
 
@@ -404,6 +435,10 @@ class HierarchicalObjectResolver {
             }
 
             // linking by parent needed even if an object is already there 
(many-to-many case)
+            // we need the row for parent attachment even if object was 
already resolved
+            if (row == null) {
+                row = processorNode.rowFromFlatRow(currentFlatRow);
+            }
             processorNode.getParentAttachmentStrategy().linkToParent(row, 
object);
 
             processorNode.setLastResolved(object);
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/access/ResultScanParentAttachmentStrategy.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/access/ResultScanParentAttachmentStrategy.java
index 176b30146..0c45bfeaa 100644
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/access/ResultScanParentAttachmentStrategy.java
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/access/ResultScanParentAttachmentStrategy.java
@@ -101,7 +101,12 @@ class ResultScanParentAttachmentStrategy implements 
ParentAttachmentStrategy {
         
         List<DataRow> rows = parentNode.getDataRows();
         if(rows == null) {
-            return;
+            if(parentNode instanceof PrefetchProcessorJointNode) {
+                rows = ((PrefetchProcessorJointNode) 
parentNode).getResolvedRows();
+            }
+            if(rows == null) {
+                return;
+            }
         }
         
         int size = objects.size();
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/util/concurrentlinkedhashmap/ConcurrentLinkedHashMap.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/util/concurrentlinkedhashmap/ConcurrentLinkedHashMap.java
index e1d98563f..772ced0e5 100644
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/util/concurrentlinkedhashmap/ConcurrentLinkedHashMap.java
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/util/concurrentlinkedhashmap/ConcurrentLinkedHashMap.java
@@ -788,6 +788,7 @@ public class ConcurrentLinkedHashMap<K, V> extends 
AbstractMap<K, V> implements
 
     @Override
     public V remove(Object key) {
+       if (key == null) return null; // this class does allow null to be used 
as a key or value (returning here prevents an NPE).
         final Node node = data.remove(key);
         if (node == null) {
             return null;
diff --git 
a/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextPrefetchMultistepIT.java
 
b/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextPrefetchMultistepIT.java
index d312f1052..6273becc0 100644
--- 
a/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextPrefetchMultistepIT.java
+++ 
b/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextPrefetchMultistepIT.java
@@ -20,14 +20,17 @@
 package org.apache.cayenne.access;
 
 import org.apache.cayenne.Fault;
+import org.apache.cayenne.ObjectContext;
 import org.apache.cayenne.ObjectId;
 import org.apache.cayenne.PersistenceState;
 import org.apache.cayenne.Persistent;
 import org.apache.cayenne.ValueHolder;
 import org.apache.cayenne.di.Inject;
 import org.apache.cayenne.query.ObjectSelect;
+import org.apache.cayenne.runtime.CayenneRuntime;
 import org.apache.cayenne.test.jdbc.DBHelper;
 import org.apache.cayenne.test.jdbc.TableHelper;
+import org.apache.cayenne.testdo.testmap.Artist;
 import org.apache.cayenne.testdo.testmap.ArtistExhibit;
 import org.apache.cayenne.testdo.testmap.Exhibit;
 import org.apache.cayenne.testdo.testmap.Gallery;
@@ -51,6 +54,9 @@ public class DataContextPrefetchMultistepIT extends 
ServerCase {
     @Inject
     protected DataContext context;
 
+    @Inject
+    protected CayenneRuntime runtime;
+
     @Inject
     protected DBHelper dbHelper;
 
@@ -286,4 +292,159 @@ public class DataContextPrefetchMultistepIT extends 
ServerCase {
         assertFalse(((ValueHolder) exhibits).isFault());
         assertEquals(0, exhibits.size());
     }
+
+
+    private Gallery createArtistWithPaintingInGallery() {
+        Artist artist = context.newObject(Artist.class);
+        artist.setArtistName("Picasso");
+
+        Painting painting = context.newObject(Painting.class);
+        painting.setPaintingTitle("Guernica");
+        artist.addToPaintingArray(painting);
+
+        Gallery gallery = context.newObject(Gallery.class);
+        gallery.setGalleryName("MOMA");
+        painting.setToGallery(gallery);
+
+        context.commitChanges();
+        return gallery;
+    }
+
+    @Test
+    public void testPrefetchAcross2RelationshipsKeepingBoth_JointAndJoint() {
+        Gallery gallery = createArtistWithPaintingInGallery();
+
+        assertNotNull(gallery.getPaintingArray().get(0).getToArtist());
+
+        // Prefetch the artist through the two relationships
+        ObjectSelect.query(Gallery.class)
+                .prefetch(Gallery.PAINTING_ARRAY.joint())
+                
.prefetch(Gallery.PAINTING_ARRAY.dot(Painting.TO_ARTIST).joint())
+                .select(context);
+
+        assertNotNull(gallery.getPaintingArray().get(0).getToArtist());
+    }
+
+    @Test
+    public void testPrefetchAcross2RelationshipsKeepingBoth_JointAndDisjoint() 
{
+        Gallery gallery = createArtistWithPaintingInGallery();
+
+        assertNotNull(gallery.getPaintingArray().get(0).getToArtist());
+
+        ObjectContext objectContext = runtime.newContext();
+
+        // Prefetch the artist through the two relationships
+        Gallery gallery2 = ObjectSelect.query(Gallery.class)
+                .prefetch(Gallery.PAINTING_ARRAY.joint())
+                
.prefetch(Gallery.PAINTING_ARRAY.dot(Painting.TO_ARTIST).disjoint())
+                .selectOne(objectContext);
+
+        assertNotNull(gallery2.getPaintingArray().get(0).getToArtist());
+        assertNotNull(gallery.getPaintingArray().get(0).getToArtist());
+    }
+
+    @Test
+    public void 
testPrefetchAcross2RelationshipsKeepingBoth_JointAndDisjointById() {
+        Gallery gallery = createArtistWithPaintingInGallery();
+
+        assertNotNull(gallery.getPaintingArray().get(0).getToArtist());
+
+        // Prefetch the artist through the two relationships
+        ObjectSelect.query(Gallery.class)
+                .prefetch(Gallery.PAINTING_ARRAY.joint())
+                
.prefetch(Gallery.PAINTING_ARRAY.dot(Painting.TO_ARTIST).disjointById())
+                .select(context);
+
+        assertNotNull(gallery.getPaintingArray().get(0).getToArtist());
+    }
+
+    @Test
+    public void testPrefetchAcross2RelationshipsKeepingBoth_DisjointAndJoint() 
{
+        Gallery gallery = createArtistWithPaintingInGallery();
+
+        assertNotNull(gallery.getPaintingArray().get(0).getToArtist());
+
+        // Prefetch the artist through the two relationships
+        ObjectSelect.query(Gallery.class)
+                .prefetch(Gallery.PAINTING_ARRAY.disjoint())
+                
.prefetch(Gallery.PAINTING_ARRAY.dot(Painting.TO_ARTIST).joint())
+                .select(context);
+
+        assertNotNull(gallery.getPaintingArray().get(0).getToArtist());
+    }
+
+    @Test
+    public void 
testPrefetchAcross2RelationshipsKeepingBoth_DisjointAndDisjoint() {
+        Gallery gallery = createArtistWithPaintingInGallery();
+
+        assertNotNull(gallery.getPaintingArray().get(0).getToArtist());
+
+        // Prefetch the artist through the two relationships
+        ObjectSelect.query(Gallery.class)
+                .prefetch(Gallery.PAINTING_ARRAY.disjoint())
+                
.prefetch(Gallery.PAINTING_ARRAY.dot(Painting.TO_ARTIST).disjoint())
+                .select(context);
+
+        assertNotNull(gallery.getPaintingArray().get(0).getToArtist());
+    }
+
+    @Test
+    public void 
testPrefetchAcross2RelationshipsKeepingBoth_DisjointAndDisjointById() {
+        Gallery gallery = createArtistWithPaintingInGallery();
+
+        assertNotNull(gallery.getPaintingArray().get(0).getToArtist());
+
+        // Prefetch the artist through the two relationships
+        ObjectSelect.query(Gallery.class)
+                .prefetch(Gallery.PAINTING_ARRAY.disjoint())
+                
.prefetch(Gallery.PAINTING_ARRAY.dot(Painting.TO_ARTIST).disjointById())
+                .select(context);
+
+        assertNotNull(gallery.getPaintingArray().get(0).getToArtist());
+    }
+
+    @Test
+    public void 
testPrefetchAcross2RelationshipsKeepingBoth_DisjointByIdAndJoint() {
+        Gallery gallery = createArtistWithPaintingInGallery();
+
+        assertNotNull(gallery.getPaintingArray().get(0).getToArtist());
+
+        // Prefetch the artist through the two relationships
+        ObjectSelect.query(Gallery.class)
+                .prefetch(Gallery.PAINTING_ARRAY.disjointById())
+                
.prefetch(Gallery.PAINTING_ARRAY.dot(Painting.TO_ARTIST).joint())
+                .select(context);
+
+        assertNotNull(gallery.getPaintingArray().get(0).getToArtist());
+    }
+
+    @Test
+    public void 
testPrefetchAcross2RelationshipsKeepingBoth_DisjointByIdAndDisjoint() {
+        Gallery gallery = createArtistWithPaintingInGallery();
+
+        assertNotNull(gallery.getPaintingArray().get(0).getToArtist());
+
+        // Prefetch the artist through the two relationships
+        ObjectSelect.query(Gallery.class)
+                .prefetch(Gallery.PAINTING_ARRAY.disjointById())
+                
.prefetch(Gallery.PAINTING_ARRAY.dot(Painting.TO_ARTIST).disjoint())
+                .select(context);
+
+        assertNotNull(gallery.getPaintingArray().get(0).getToArtist());
+    }
+
+    @Test
+    public void 
testPrefetchAcross2RelationshipsKeepingBoth_DisjointByIdAndDisjointById() {
+        Gallery gallery = createArtistWithPaintingInGallery();
+
+        assertNotNull(gallery.getPaintingArray().get(0).getToArtist());
+
+        // Prefetch the artist through the two relationships
+        ObjectSelect.query(Gallery.class)
+                .prefetch(Gallery.PAINTING_ARRAY.disjointById())
+                
.prefetch(Gallery.PAINTING_ARRAY.dot(Painting.TO_ARTIST).disjointById())
+                .select(context);
+
+        assertNotNull(gallery.getPaintingArray().get(0).getToArtist());
+    }
 }

Reply via email to