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


The following commit(s) were added to refs/heads/master by this push:
     new fe0618bbe CAY-3006 Can't match a "self" property of an entity with a 
compound PK
fe0618bbe is described below

commit fe0618bbe27dd5f431eb6f201fa39b033bb717b5
Author: Andrus Adamchik <[email protected]>
AuthorDate: Sun Aug 30 16:43:11 2026 -0400

    CAY-3006 Can't match a "self" property of an entity with a compound PK
---
 RELEASE-NOTES.txt                                  |  1 +
 .../translator/select/QualifierTranslator.java     | 46 +++++++++++++--
 .../translator/select/QualifierTranslatorIT.java   | 67 ++++++++++++++++++++++
 3 files changed, 108 insertions(+), 6 deletions(-)

diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt
index 391fe3fbc..d383ab82c 100644
--- a/RELEASE-NOTES.txt
+++ b/RELEASE-NOTES.txt
@@ -24,6 +24,7 @@ CAY-3001 Modeler: Auto-increment attribute is not picked up
 CAY-3002 Modeler: NPE after clicking path chooser
 CAY-3003 Modeler: NPE editing unmapped ObjAttribute
 CAY-3005 ResultIterator.allRows() doesn't convert to DataObjects
+CAY-3006 Can't match a "self" property of an entity with a compound PK
 
 ----------------------------------
 Release: 5.0-M3
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/access/translator/select/QualifierTranslator.java
 
b/cayenne/src/main/java/org/apache/cayenne/access/translator/select/QualifierTranslator.java
index 78cf33f8d..17b5ab913 100644
--- 
a/cayenne/src/main/java/org/apache/cayenne/access/translator/select/QualifierTranslator.java
+++ 
b/cayenne/src/main/java/org/apache/cayenne/access/translator/select/QualifierTranslator.java
@@ -283,11 +283,11 @@ class QualifierTranslator implements TraversalHandler {
                 ASTFullObject fullObject = (ASTFullObject) node;
                 if (fullObject.getOperandCount() == 0) {
                     Collection<DbAttribute> dbAttributes = 
context.getMetadata().getDbEntity().getPrimaryKeys();
+                    String alias = 
context.getTableTree().aliasForPath(CayennePath.EMPTY_PATH);
                     if (dbAttributes.size() > 1) {
-                        throw new CayenneRuntimeException("Unable to translate 
reference on entity with more than one PK.");
+                        return createMultiPkMatch(node, parentNode, 
dbAttributes, alias);
                     }
                     DbAttribute attribute = dbAttributes.iterator().next();
-                    String alias = 
context.getTableTree().aliasForPath(CayennePath.EMPTY_PATH);
                     return table(alias).column(attribute).build();
                 } else {
                     return null;
@@ -404,6 +404,36 @@ class QualifierTranslator implements TraversalHandler {
         return null;
     }
 
+    /**
+     * Matches the root entity referenced as a whole (i.e. a bare {@link 
ASTFullObject}) against an
+     * {@link ObjectId} or a {@link Persistent}, expanding the comparison over 
all the PK columns. This is the
+     * root-entity counterpart of {@link 
#createMultiAttributeMatch(Expression, Expression, PathTranslationResult)}.
+     */
+    private Node createMultiPkMatch(Expression node, Expression parentNode,
+                                    Collection<DbAttribute> pkAttributes, 
String alias) {
+        if (parentNode == null) {
+            throw new CayenneRuntimeException("Unable to translate reference 
on entity with more than one PK.");
+        }
+
+        Map<String, Object> valueSnapshot = 
getMultiAttributeValueSnapshot(node, parentNode);
+        Node multiValueComparison = buildMultiValueComparison(pkAttributes, 
alias, valueSnapshot);
+
+        // replace current node with multi value comparison
+        Node currentNodeParent = currentNode.getParent();
+        currentNodeParent.replaceChild(currentNodeParent.getChildrenCount() - 
1, multiValueComparison);
+        multiValueComparison.setParent(currentNodeParent);
+        currentNode = currentNodeParent;
+
+        // we should skip all related nodes as we build this part of the tree 
manually
+        expressionsToSkip.add(node);
+        expressionsToSkip.add(parentNode);
+        for (int i = 0; i < parentNode.getOperandCount(); i++) {
+            expressionsToSkip.add(parentNode.getOperand(i));
+        }
+
+        return null;
+    }
+
     private Map<String, Object> getMultiAttributeValueSnapshot(Expression 
node, Expression parentNode) {
         int siblings = parentNode.getOperandCount();
         for (int i = 0; i < siblings; i++) {
@@ -427,13 +457,17 @@ class QualifierTranslator implements TraversalHandler {
     }
 
     private Node buildMultiValueComparison(PathTranslationResult result, 
Map<String, Object> valueSnapshot) {
-        ExpressionNodeBuilder expressionNodeBuilder = null;
-        ExpressionNodeBuilder eq;
-
         CayennePath path = result.getLastAttributePath();
         String alias = context.getTableTree().aliasForPath(path);
+        return buildMultiValueComparison(result.getDbAttributes(), alias, 
valueSnapshot);
+    }
+
+    private Node buildMultiValueComparison(Collection<DbAttribute> attributes, 
String alias,
+                                           Map<String, Object> valueSnapshot) {
+        ExpressionNodeBuilder expressionNodeBuilder = null;
+        ExpressionNodeBuilder eq;
 
-        for (DbAttribute attribute : result.getDbAttributes()) {
+        for (DbAttribute attribute : attributes) {
             Object nextValue = valueSnapshot.get(attribute.getName());
             eq = table(alias).column(attribute).eq(value(nextValue));
             if (expressionNodeBuilder == null) {
diff --git 
a/cayenne/src/test/java/org/apache/cayenne/access/translator/select/QualifierTranslatorIT.java
 
b/cayenne/src/test/java/org/apache/cayenne/access/translator/select/QualifierTranslatorIT.java
index 9191ab9bb..668fbd6ae 100644
--- 
a/cayenne/src/test/java/org/apache/cayenne/access/translator/select/QualifierTranslatorIT.java
+++ 
b/cayenne/src/test/java/org/apache/cayenne/access/translator/select/QualifierTranslatorIT.java
@@ -21,8 +21,11 @@ package org.apache.cayenne.access.translator.select;
 import org.apache.cayenne.access.sqlbuilder.SQLGenerationVisitor;
 import org.apache.cayenne.access.sqlbuilder.DefaultSQLAppendable;
 import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
+import org.apache.cayenne.ObjectId;
 import org.apache.cayenne.exp.ExpressionFactory;
+import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.query.ObjectSelect;
+import org.apache.cayenne.query.SelectById;
 import org.apache.cayenne.runtime.CayenneRuntime;
 import org.apache.cayenne.test.jdbc.TableHelper;
 import org.apache.cayenne.testdo.compound.CompoundFkTestEntity;
@@ -34,9 +37,13 @@ import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
 import java.util.List;
+import java.util.Map;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 public class QualifierTranslatorIT {
 
@@ -120,4 +127,64 @@ public class QualifierTranslatorIT {
 
     }
 
+    /**
+     * A bare "self" reference on an entity with a compound PK must expand 
over all PK columns,
+     * the same way a to-one relationship path does.
+     */
+    @Test
+    public void compoundPKSelfWithObjectId() {
+        ObjectId id = ObjectId.of("CompoundPkTestEntity", Map.of("KEY1", 
"PK1", "KEY2", "PK2"));
+
+        ObjectSelect<CompoundPkTestEntity> query = 
ObjectSelect.query(CompoundPkTestEntity.class)
+                .where(CompoundPkTestEntity.SELF.eqId(id));
+
+        assertEquals(" cpt.KEY1 = 'PK1' AND cpt.KEY2 = 'PK2'", 
translate(query));
+    }
+
+    @Test
+    public void compoundPKSelfWithPersistent() {
+        CompoundPkTestEntity testEntity = 
ObjectSelect.query(CompoundPkTestEntity.class)
+                
.where(CompoundPkTestEntity.NAME.eq("BBB")).selectOne(env.context());
+        assertNotNull(testEntity);
+
+        ObjectSelect<CompoundPkTestEntity> query = 
ObjectSelect.query(CompoundPkTestEntity.class)
+                .where(CompoundPkTestEntity.SELF.eqId(testEntity));
+
+        assertEquals(" cpt.KEY1 = 'PK1' AND cpt.KEY2 = 'PK2'", 
translate(query));
+    }
+
+    @Test
+    public void compoundPKSelfSelectsTheRightRow() {
+        ObjectId id = ObjectId.of("CompoundPkTestEntity", Map.of("KEY1", 
"PK3", "KEY2", "PK4"));
+
+        CompoundPkTestEntity viaSelf = 
ObjectSelect.query(CompoundPkTestEntity.class)
+                
.where(CompoundPkTestEntity.SELF.eqId(id)).selectOne(env.context());
+        assertNotNull(viaSelf);
+        assertEquals("CCC", viaSelf.getName());
+
+        // must agree with the dedicated by-id query
+        assertSame(SelectById.queryObjectId(CompoundPkTestEntity.class, 
id).selectOne(env.context()), viaSelf);
+    }
+
+    /**
+     * A compound-PK "self" reference still cannot be matched against a scalar 
- there is no single
+     * PK column to compare it to.
+     */
+    @Test
+    public void compoundPKSelfWithScalarFails() {
+        ObjectSelect<CompoundPkTestEntity> query = 
ObjectSelect.query(CompoundPkTestEntity.class)
+                .where(CompoundPkTestEntity.SELF.eqId("PK1"));
+
+        CayenneRuntimeException e = 
assertThrows(CayenneRuntimeException.class, () -> translate(query));
+        assertTrue(e.getMessage().contains("Multi attribute ObjPath isn't 
matched with valid value"), e.getMessage());
+    }
+
+    private String translate(ObjectSelect<?> query) {
+        SelectTranslatorContext context = new SelectTranslatorContext(
+                query, runtime.getDataDomain().getDefaultNode().getAdapter(), 
env.context().getEntityResolver(), null);
+        Node node = 
context.getQualifierTranslator().translate(query.getWhere());
+        SQLGenerationVisitor visitor = new SQLGenerationVisitor(new 
DefaultSQLAppendable(null), null);
+        node.visit(visitor);
+        return visitor.getSQLString();
+    }
 }

Reply via email to