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

asf-gitbox-commits pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/cayenne.git

commit 1fa29ca1eba950b36ee1c93ad70659b46f6a8278
Author: Andrus Adamchik <[email protected]>
AuthorDate: Mon Jul 13 18:42:42 2026 -0400

    CAY-2977 DBImport confused by multi-key relationships
---
 .../dbsync/reverse/dbload/DbLoadDataStore.java     |  5 +-
 .../cayenne/dbsync/reverse/dbload/ExportedKey.java | 15 ++++
 .../dbsync/reverse/dbload/ExportedKeyLoaderIT.java |  5 +-
 .../reverse/dbload/RelationshipsLoaderIT.java      | 96 ++++++++++++++++++++--
 cayenne/src/test/resources/compound.map.xml        | 21 +++++
 5 files changed, 132 insertions(+), 10 deletions(-)

diff --git 
a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/dbload/DbLoadDataStore.java
 
b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/dbload/DbLoadDataStore.java
index c6513d7b1..422f96f08 100644
--- 
a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/dbload/DbLoadDataStore.java
+++ 
b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/dbload/DbLoadDataStore.java
@@ -84,10 +84,11 @@ public class DbLoadDataStore extends DataMap {
     }
 
     void addExportedKey(ExportedKey key) {
-        Set<ExportedKey> exportedKeys = this.exportedKeys.get(key.getStrKey());
+        // group by the FK constraint, so that all columns of a multi-column 
FK end up in a single relationship
+        Set<ExportedKey> exportedKeys = 
this.exportedKeys.get(key.getGroupKey());
         if (exportedKeys == null) {
             exportedKeys = new TreeSet<>();
-            this.exportedKeys.put(key.getStrKey(), exportedKeys);
+            this.exportedKeys.put(key.getGroupKey(), exportedKeys);
         }
         exportedKeys.add(key);
     }
diff --git 
a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/dbload/ExportedKey.java
 
b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/dbload/ExportedKey.java
index 374c36f1d..2ca48a9a7 100644
--- 
a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/dbload/ExportedKey.java
+++ 
b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/dbload/ExportedKey.java
@@ -116,6 +116,21 @@ public class ExportedKey implements 
Comparable<ExportedKey> {
         return pk + " <- " + fk;
     }
 
+    /**
+     * Returns a key that identifies the single FK constraint this row belongs 
to, so that all columns of a
+     * multi-column FK are grouped into one relationship. Uses the FK 
constraint name (FK_NAME) reported by the
+     * driver; falls back to the per-column {@link #getStrKey()} when the name 
is unavailable, preserving the
+     * historical behavior for drivers that don't report constraint names.
+     */
+    String getGroupKey() {
+        String fkName = fk.getName();
+        if (Util.isEmptyString(fkName)) {
+            return getStrKey();
+        }
+        return fk.getCatalog() + "." + fk.getSchema() + "." + fk.getTable() + 
"." + fkName
+                + " -> " + pk.getCatalog() + "." + pk.getSchema() + "." + 
pk.getTable();
+    }
+
     public static class KeyData implements Comparable<KeyData> {
         private final String catalog;
         private final String schema;
diff --git 
a/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/reverse/dbload/ExportedKeyLoaderIT.java
 
b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/reverse/dbload/ExportedKeyLoaderIT.java
index fa902af7c..84ad8fb8b 100644
--- 
a/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/reverse/dbload/ExportedKeyLoaderIT.java
+++ 
b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/reverse/dbload/ExportedKeyLoaderIT.java
@@ -90,8 +90,9 @@ public class ExportedKeyLoaderIT extends BaseLoaderIT {
 
     private ExportedKey findArtistExportedKey() {
         for(Map.Entry<String, Set<ExportedKey>> entry : 
store.getExportedKeysEntrySet()) {
-            if(entry.getKey().toUpperCase().endsWith(".ARTIST_ID")) {
-                return entry.getValue().iterator().next();
+            ExportedKey key = entry.getValue().iterator().next();
+            if("ARTIST_ID".equalsIgnoreCase(key.getFk().getColumn())) {
+                return key;
             }
         }
 
diff --git 
a/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/reverse/dbload/RelationshipsLoaderIT.java
 
b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/reverse/dbload/RelationshipsLoaderIT.java
index bd11d9d75..445b525c0 100644
--- 
a/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/reverse/dbload/RelationshipsLoaderIT.java
+++ 
b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/reverse/dbload/RelationshipsLoaderIT.java
@@ -19,16 +19,17 @@
 
 package org.apache.cayenne.dbsync.reverse.dbload;
 
-import java.sql.DatabaseMetaData;
-import java.util.Collection;
-
 import org.apache.cayenne.dbsync.naming.DefaultObjectNameGenerator;
+import org.apache.cayenne.map.DbEntity;
 import org.apache.cayenne.map.DbRelationship;
 import org.junit.jupiter.api.Test;
 
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertTrue;
+import java.sql.DatabaseMetaData;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
 
 public class RelationshipsLoaderIT extends BaseLoaderIT {
 
@@ -79,6 +80,89 @@ public class RelationshipsLoaderIT extends BaseLoaderIT {
         assertTrue(oneToOne.isToDependentPK(), "Relationship to PAINTING_INFO 
must be to-one");
     }
 
+    @Test
+    public void compoundFkLoad() throws Exception {
+        if (!accessStackAdapter.supportsFKConstraints()) {
+            return;
+        }
+
+        DatabaseMetaData metaData = connection.getMetaData();
+        DbLoaderDelegate delegate = new DefaultDbLoaderDelegate();
+
+        new EntityLoader(adapter, EMPTY_CONFIG, delegate).load(metaData, 
store);
+        new AttributeLoader(adapter, EMPTY_CONFIG, delegate).load(metaData, 
store);
+        new PrimaryKeyLoader(EMPTY_CONFIG, delegate).load(metaData, store);
+        new ExportedKeyLoader(EMPTY_CONFIG, delegate).load(metaData, store);
+
+        new RelationshipLoader(EMPTY_CONFIG, delegate, new 
DefaultObjectNameGenerator()).load(metaData, store);
+
+        // COMPOUND_FK_TEST has a 2-column FK (F_KEY1, F_KEY2) -> 
COMPOUND_PK_TEST (KEY1, KEY2)
+        DbEntity fkEntity = getDbEntity("COMPOUND_FK_TEST");
+        assertNotNull(fkEntity, "COMPOUND_FK_TEST entity was not loaded");
+
+        List<DbRelationship> toCompoundPk = relationshipsTo(fkEntity, 
"COMPOUND_PK_TEST");
+        assertEquals(1, toCompoundPk.size(),
+                "The 2-column FK must produce a single to-one relationship, 
not one per column");
+        assertEquals(2, toCompoundPk.get(0).getJoins().size(),
+                "The relationship must carry both FK column joins");
+
+        // the reverse (to-many) relationship must be a single compound 
relationship as well
+        DbEntity pkEntity = getDbEntity("COMPOUND_PK_TEST");
+        assertNotNull(pkEntity, "COMPOUND_PK_TEST entity was not loaded");
+
+        List<DbRelationship> toCompoundFk = relationshipsTo(pkEntity, 
"COMPOUND_FK_TEST");
+        assertEquals(1, toCompoundFk.size(),
+                "The reverse of the 2-column FK must be a single to-many 
relationship, not one per column");
+        assertEquals(2, toCompoundFk.get(0).getJoins().size(),
+                "The reverse relationship must carry both FK column joins");
+    }
+
+    @Test
+    public void twoIndependentFksLoad() throws Exception {
+        if (!accessStackAdapter.supportsFKConstraints()) {
+            return;
+        }
+
+        DatabaseMetaData metaData = connection.getMetaData();
+        DbLoaderDelegate delegate = new DefaultDbLoaderDelegate();
+
+        new EntityLoader(adapter, EMPTY_CONFIG, delegate).load(metaData, 
store);
+        new AttributeLoader(adapter, EMPTY_CONFIG, delegate).load(metaData, 
store);
+        new PrimaryKeyLoader(EMPTY_CONFIG, delegate).load(metaData, store);
+        new ExportedKeyLoader(EMPTY_CONFIG, delegate).load(metaData, store);
+
+        new RelationshipLoader(EMPTY_CONFIG, delegate, new 
DefaultObjectNameGenerator()).load(metaData, store);
+
+        // TWO_FK_B has two separate single-column FKs (A_ID1, A_ID2) -> 
TWO_FK_A (ID)
+        DbEntity bEntity = getDbEntity("TWO_FK_B");
+        assertNotNull(bEntity, "TWO_FK_B entity was not loaded");
+
+        List<DbRelationship> toA = relationshipsTo(bEntity, "TWO_FK_A");
+        assertEquals(2, toA.size(), "Two independent FKs must produce two 
separate relationships");
+        for (DbRelationship rel : toA) {
+            assertEquals(1, rel.getJoins().size(), "Each single-column FK 
relationship must have exactly one join");
+        }
+
+        DbEntity aEntity = getDbEntity("TWO_FK_A");
+        assertNotNull(aEntity, "TWO_FK_A entity was not loaded");
+
+        List<DbRelationship> toB = relationshipsTo(aEntity, "TWO_FK_B");
+        assertEquals(2, toB.size(), "Two independent FKs must produce two 
separate reverse relationships");
+        for (DbRelationship rel : toB) {
+            assertEquals(1, rel.getJoins().size(), "Each reverse relationship 
must have exactly one join");
+        }
+    }
+
+    private List<DbRelationship> relationshipsTo(DbEntity entity, String 
targetName) {
+        List<DbRelationship> result = new ArrayList<>();
+        for (DbRelationship rel : entity.getRelationships()) {
+            if (targetName.equalsIgnoreCase(rel.getTargetEntityName())) {
+                result.add(rel);
+            }
+        }
+        return result;
+    }
+
 //    private void assertUniqueConstraintsInRelationships(DataMap map) {
         // unfortunately JDBC metadata doesn't provide info for UNIQUE
         // constraints....
diff --git a/cayenne/src/test/resources/compound.map.xml 
b/cayenne/src/test/resources/compound.map.xml
index 5b3d92b1a..c70bef5dd 100644
--- a/cayenne/src/test/resources/compound.map.xml
+++ b/cayenne/src/test/resources/compound.map.xml
@@ -43,6 +43,15 @@
                <db-attribute name="order_line_number" type="INTEGER" 
isPrimaryKey="true" isMandatory="true"/>
                <db-attribute name="order_number" type="INTEGER" 
isPrimaryKey="true" isMandatory="true"/>
        </db-entity>
+       <db-entity name="TWO_FK_A">
+               <db-attribute name="ID" type="INTEGER" isPrimaryKey="true" 
isMandatory="true"/>
+               <db-attribute name="NAME" type="VARCHAR" length="100"/>
+       </db-entity>
+       <db-entity name="TWO_FK_B">
+               <db-attribute name="ID" type="INTEGER" isPrimaryKey="true" 
isMandatory="true"/>
+               <db-attribute name="A_ID1" type="INTEGER"/>
+               <db-attribute name="A_ID2" type="INTEGER"/>
+       </db-entity>
        <obj-entity name="CharFkTestEntity" 
className="org.apache.cayenne.testdo.compound.CharFkTestEntity" 
dbEntityName="CHAR_FK_TEST">
                <obj-attribute name="name" type="java.lang.String" 
db-attribute-path="NAME"/>
        </obj-entity>
@@ -100,6 +109,18 @@
                <db-attribute-pair source="order_number" target="order_number"/>
                <db-attribute-pair source="order_line_number" 
target="order_line_number"/>
        </db-relationship>
+       <db-relationship name="toA1" source="TWO_FK_B" target="TWO_FK_A">
+               <db-attribute-pair source="A_ID1" target="ID"/>
+       </db-relationship>
+       <db-relationship name="toA2" source="TWO_FK_B" target="TWO_FK_A">
+               <db-attribute-pair source="A_ID2" target="ID"/>
+       </db-relationship>
+       <db-relationship name="bs1" source="TWO_FK_A" target="TWO_FK_B" 
toMany="true">
+               <db-attribute-pair source="ID" target="A_ID1"/>
+       </db-relationship>
+       <db-relationship name="bs2" source="TWO_FK_A" target="TWO_FK_B" 
toMany="true">
+               <db-attribute-pair source="ID" target="A_ID2"/>
+       </db-relationship>
        <obj-relationship name="toCharPK" source="CharFkTestEntity" 
target="CharPkTestEntity" db-relationship-path="toCharPK"/>
        <obj-relationship name="charFKs" source="CharPkTestEntity" 
target="CharFkTestEntity" db-relationship-path="charFKs"/>
        <obj-relationship name="toCompoundPk" source="CompoundFkTestEntity" 
target="CompoundPkTestEntity" db-relationship-path="toCompoundPk"/>

Reply via email to