You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cayenne.apache.org by nt...@apache.org on 2026/09/11 12:03:33 UTC

(cayenne) branch master updated: CAY-2911 Vertical Inheritance: Column Select Sometimes Null With Vertically Inherited Entity

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

stariy95 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 9e36902fd CAY-2911 Vertical Inheritance: Column Select Sometimes Null With Vertically Inherited Entity
9e36902fd is described below

commit 9e36902fd840b148ca2b9e8c71ef39d8faf7fafe
Author: ntimofeev <st...@gmail.com>
AuthorDate: Fri Sep 11 15:03:23 2026 +0300

    CAY-2911 Vertical Inheritance: Column Select Sometimes Null With Vertically Inherited Entity
---
 RELEASE-NOTES.txt                                  |  1 +
 .../select/DescriptorColumnExtractor.java          | 13 ++-
 .../cayenne/access/VerticalInheritanceIT.java      | 93 +++++++++++++---------
 ...riptorColumnExtractorVerticalInheritanceIT.java | 90 +++++++++++++++++++++
 4 files changed, 156 insertions(+), 41 deletions(-)

diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt
index 454e0bd68..13c1f3e40 100644
--- a/RELEASE-NOTES.txt
+++ b/RELEASE-NOTES.txt
@@ -29,6 +29,7 @@ CAY-3017 ObjectContext should not be a DataChannel
 Bug Fixes:
 
 CAY-2832 SelectActionIT is failing on GitHub Actions
+CAY-2911 Vertical Inheritance: Column Select Sometimes Null With Vertically Inherited Entity
 CAY-2996 Javadocs are missing from the central artifacts
 CAY-2999 Modeler: A restricted method in java.lang.System has been called
 CAY-3001 Modeler: Auto-increment attribute is not picked up
diff --git a/cayenne/src/main/java/org/apache/cayenne/access/translator/select/DescriptorColumnExtractor.java b/cayenne/src/main/java/org/apache/cayenne/access/translator/select/DescriptorColumnExtractor.java
index 86094a32a..cd445e076 100644
--- a/cayenne/src/main/java/org/apache/cayenne/access/translator/select/DescriptorColumnExtractor.java
+++ b/cayenne/src/main/java/org/apache/cayenne/access/translator/select/DescriptorColumnExtractor.java
@@ -151,8 +151,17 @@ class DescriptorColumnExtractor extends BaseColumnExtractor implements PropertyV
 
         int count = result.getDbAttributes().size();
         for(int i=0; i<count; i++) {
-            processTranslationResult(result, i);
-            addEntityResultField(result.getDbAttributes().get(i));
+            if(processTranslationResult(result, i) == null) {
+                // the column was already emitted for an earlier property, and its result field with it
+                continue;
+            }
+            DbAttribute dbAttribute = result.getDbAttributes().get(i);
+            if(count > 1) {
+                // See CAY-2911, and CAY-2552 for the same fix in visitAttribute().
+                addEntityResultField(result.getAttributePaths().get(i).dot(dbAttribute.getName()));
+            } else {
+                addEntityResultField(dbAttribute);
+            }
         }
 
         return true;
diff --git a/cayenne/src/test/java/org/apache/cayenne/access/VerticalInheritanceIT.java b/cayenne/src/test/java/org/apache/cayenne/access/VerticalInheritanceIT.java
index a0216d5fb..91ef5f292 100644
--- a/cayenne/src/test/java/org/apache/cayenne/access/VerticalInheritanceIT.java
+++ b/cayenne/src/test/java/org/apache/cayenne/access/VerticalInheritanceIT.java
@@ -35,6 +35,7 @@ import org.apache.cayenne.unit.CayenneTestsEnv;
 import org.junit.jupiter.api.extension.RegisterExtension;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
 
 import java.sql.SQLException;
@@ -61,6 +62,8 @@ public class VerticalInheritanceIT {
 
 	TableHelper ivConcreteTable;
 
+	TableHelper ivRootTable;
+
 	@BeforeEach
 	public void setup() {
 		runtime = env.runtime();
@@ -68,6 +71,8 @@ public class VerticalInheritanceIT {
 				.setColumnTypes(Types.INTEGER, Types.INTEGER, Types.CHAR);
 		ivConcreteTable = env.table("IV_CONCRETE").setColumns("ID", "NAME", "RELATED_ABSTRACT_ID")
 				.setColumnTypes(Types.INTEGER, Types.VARCHAR, Types.INTEGER);
+		ivRootTable = env.table("IV_ROOT", "ID", "NAME", "DISCRIMINATOR")
+				.setColumnTypes(Types.INTEGER, Types.VARCHAR, Types.VARCHAR);
 	}
 
 	@AfterEach
@@ -81,9 +86,6 @@ public class VerticalInheritanceIT {
 
     @Test
 	public void insertRoot() throws Exception {
-
-		TableHelper ivRootTable = env.table("IV_ROOT", "ID", "NAME", "DISCRIMINATOR");
-
 		assertEquals(0, ivRootTable.getRowCount());
 
 		IvRoot root = env.context().newObject(IvRoot.class);
@@ -103,8 +105,6 @@ public class VerticalInheritanceIT {
     @Test
 	public void insertSub1() throws Exception {
 
-		TableHelper ivRootTable = env.table("IV_ROOT", "ID", "NAME", "DISCRIMINATOR");
-
 		TableHelper ivSub1Table = env.table("IV_SUB1", "ID", "SUB1_NAME");
 
 		IvSub1 sub1 = env.context().newObject(IvSub1.class);
@@ -144,9 +144,6 @@ public class VerticalInheritanceIT {
 
     @Test
 	public void insertSub2() throws Exception {
-
-		TableHelper ivRootTable = env.table("IV_ROOT", "ID", "NAME", "DISCRIMINATOR");
-
 		TableHelper ivSub2Table = env.table("IV_SUB2", "ID", "SUB2_NAME", "SUB2_ATTR");
 
 		IvSub2 sub2 = env.context().newObject(IvSub2.class);
@@ -216,9 +213,6 @@ public class VerticalInheritanceIT {
 	 */
 	@Test
 	public void validationOnInsertSub3Ok() throws Exception {
-
-		TableHelper ivRootTable = env.table("IV_ROOT", "ID", "NAME", "DISCRIMINATOR");
-
 		TableHelper ivSub3Table = env.table("IV_SUB3", "ID", "IV_ROOT_ID");
 
 		IvSub3 sub3 = env.context().newObject(IvSub3.class);
@@ -235,7 +229,6 @@ public class VerticalInheritanceIT {
 	 */
 	@Test
 	public void updateRelationSub3() throws Exception {
-		TableHelper ivRootTable = env.table("IV_ROOT", "ID", "NAME", "DISCRIMINATOR");
 		ivRootTable.insert(1, "root1", null);
 		ivRootTable.insert(2, "root2", null);
 		ivRootTable.insert(3, "name", "IvSub3");
@@ -266,9 +259,6 @@ public class VerticalInheritanceIT {
 
     @Test
 	public void insertSub1Sub1() throws Exception {
-
-		TableHelper ivRootTable = env.table("IV_ROOT", "ID", "NAME", "DISCRIMINATOR");
-
 		TableHelper ivSub1Table = env.table("IV_SUB1", "ID", "SUB1_NAME", "SUB1_PRICE");
 
 		TableHelper ivSub1Sub1Table = env.table("IV_SUB1_SUB1", "ID", "SUB1_SUB1_NAME", "SUB1_SUB1_PRICE");
@@ -306,10 +296,6 @@ public class VerticalInheritanceIT {
 
     @Test
 	public void selectQuerySuperSub() throws Exception {
-
-		TableHelper ivRootTable = env.table("IV_ROOT").setColumns("ID", "NAME", "DISCRIMINATOR").setColumnTypes(
-				Types.INTEGER, Types.VARCHAR, Types.VARCHAR);
-
 		TableHelper ivSub1Table = env.table("IV_SUB1", "ID", "SUB1_NAME");
 
 		// insert
@@ -346,10 +332,6 @@ public class VerticalInheritanceIT {
 
     @Test
 	public void selectQueryDeepAndWide() throws Exception {
-
-		TableHelper ivRootTable = env.table("IV_ROOT").setColumns("ID", "NAME", "DISCRIMINATOR").setColumnTypes(
-				Types.INTEGER, Types.VARCHAR, Types.VARCHAR);
-
 		TableHelper ivSub1Table = env.table("IV_SUB1", "ID", "SUB1_NAME");
 
 		TableHelper ivSub2Table = env.table("IV_SUB2", "ID", "SUB2_NAME");
@@ -412,10 +394,6 @@ public class VerticalInheritanceIT {
 
     @Test
 	public void selectQueryMiddleLeaf() throws Exception {
-
-		TableHelper ivRootTable = env.table("IV_ROOT").setColumns("ID", "NAME", "DISCRIMINATOR").setColumnTypes(
-				Types.INTEGER, Types.VARCHAR, Types.VARCHAR);
-
 		TableHelper ivSub1Table = env.table("IV_SUB1", "ID", "SUB1_NAME");
 
 		TableHelper ivSub2Table = env.table("IV_SUB2", "ID", "SUB2_NAME");
@@ -467,10 +445,6 @@ public class VerticalInheritanceIT {
 
     @Test
 	public void deleteMix() throws Exception {
-
-		TableHelper ivRootTable = env.table("IV_ROOT").setColumns("ID", "NAME", "DISCRIMINATOR").setColumnTypes(
-				Types.INTEGER, Types.VARCHAR, Types.VARCHAR);
-
 		TableHelper ivSub1Table = env.table("IV_SUB1", "ID", "SUB1_NAME");
 
 		TableHelper ivSub2Table = env.table("IV_SUB2", "ID", "SUB2_NAME");
@@ -1036,8 +1010,6 @@ public class VerticalInheritanceIT {
 
 	@Test
 	public void countEjbqlQuery() throws Exception {
-		TableHelper ivRootTable = env.table("IV_ROOT", "ID", "NAME", "DISCRIMINATOR");
-
 		TableHelper ivSub1Table = env.table("IV_SUB1", "ID", "SUB1_NAME");
 
 		TableHelper ivSub2Table = env.table("IV_SUB2", "ID", "SUB2_ATTR", "SUB2_NAME");
@@ -1075,8 +1047,6 @@ public class VerticalInheritanceIT {
 
 	@Test
 	public void columnSelectVerticalInheritanceSub1() throws SQLException {
-		TableHelper ivRootTable = env.table("IV_ROOT", "ID", "NAME", "DISCRIMINATOR");
-
 		TableHelper ivSub1Table = env.table("IV_SUB1", "ID", "SUB1_NAME", "SUB1_PRICE");
 
 		TableHelper ivSub1Sub1Table = env.table("IV_SUB1_SUB1", "ID", "SUB1_SUB1_NAME", "SUB1_SUB1_PRICE");
@@ -1120,8 +1090,6 @@ public class VerticalInheritanceIT {
 
 	@Test
 	public void columnSelectVerticalInheritanceSub1Sub1() throws SQLException {
-		TableHelper ivRootTable = env.table("IV_ROOT", "ID", "NAME", "DISCRIMINATOR");
-
 		TableHelper ivSub1Table = env.table("IV_SUB1", "ID", "SUB1_NAME", "SUB1_PRICE");
 
 		TableHelper ivSub1Sub1Table = env.table("IV_SUB1_SUB1", "ID", "SUB1_SUB1_NAME", "SUB1_SUB1_PRICE");
@@ -1167,7 +1135,7 @@ public class VerticalInheritanceIT {
 	@Test
 	public void insertTwoGenericVerticalInheritanceObjects() {
 		// Generic DataObjects play nicer with a DataContext
-		final DataContext dataContext = (DataContext) env.context();
+		final DataContext dataContext = env.context();
 
 		final Persistent girlEmma = dataContext.newObject("GenGirl");
 		final Persistent boyLuke = dataContext.newObject("GenBoy");
@@ -1271,7 +1239,6 @@ public class VerticalInheritanceIT {
 
 	@Test
 	public void updateFlattenedAttributeOfThreeLevelInheritanceChild() throws SQLException {
-		TableHelper ivRootTable = env.table("IV_ROOT", "ID", "DISCRIMINATOR");
 		TableHelper ivSub1Table = env.table("IV_SUB1", "ID");
 		TableHelper ivSub1Sub1Table = env.table("IV_SUB1_SUB1", "ID", "SUB1_SUB1_NAME");
 
@@ -1290,4 +1257,52 @@ public class VerticalInheritanceIT {
 		IvSub1Sub1 reread = ObjectSelect.query(IvSub1Sub1.class).where(IvSub1Sub1.SELF.eqId(1)).selectOne(cleanContext);
 		assertEquals("sub1sub1name-updated", reread.getSub1Sub1Name());
 	}
+
+	/**
+	 * See CAY-2911.
+	 */
+	@Test
+	public void selectColumnsSelf() throws SQLException {
+		TableHelper ivSub1Table = env.table("IV_SUB1", "ID", "SUB1_NAME", "SUB1_PRICE");
+		TableHelper ivSub2Table = env.table("IV_SUB2", "ID", "SUB2_ATTR", "SUB2_NAME");
+
+		ivRootTable.insert(1, "root", null);
+		ivRootTable.insert(2, "s1", "IvSub1");
+		ivSub1Table.insert(2, "sub1name", 42.0);
+		ivRootTable.insert(3, "s2", "IvSub2");
+		ivSub2Table.insert(3, "sub2attr", "sub2name");
+
+		ObjectContext freshContext = runtime.newContext();
+		List<IvRoot> results = ObjectSelect.query(IvRoot.class)
+				.column(IvRoot.SELF)
+				.orderBy(IvRoot.NAME.asc())
+				.select(freshContext);
+
+		assertEquals(3, results.size());
+		assertFalse(results.contains(null), () -> "column select returned a null object: " + results);
+
+		assertEquals(IvRoot.class, results.get(0).getClass());
+		assertEquals("root", results.get(0).getName());
+		assertInstanceOf(IvSub1.class, results.get(1));
+		assertInstanceOf(IvSub2.class, results.get(2));
+	}
+
+	/**
+	 * See CAY-2911.
+	 */
+	@Test
+	public void selectColumnsSelfAndScalar() throws SQLException {
+		TableHelper ivSub3Table = env.table("IV_SUB3", "ID", "IV_ROOT_ID");
+
+		ivRootTable.insert(1, "sub3name", "IvSub3");
+		ivSub3Table.insert(1, 1);
+
+		ObjectContext freshContext = runtime.newContext();
+		Object[] row = ObjectSelect.query(IvSub3.class)
+				.columns(IvSub3.SELF, IvSub3.NAME)
+				.selectOne(freshContext);
+
+		assertInstanceOf(IvSub3.class, row[0]);
+		assertEquals("sub3name", row[1], () -> "scalar slot held " + row[1] + ", segment offset is wrong");
+	}
 }
diff --git a/cayenne/src/test/java/org/apache/cayenne/access/translator/select/DescriptorColumnExtractorVerticalInheritanceIT.java b/cayenne/src/test/java/org/apache/cayenne/access/translator/select/DescriptorColumnExtractorVerticalInheritanceIT.java
new file mode 100644
index 000000000..464208a75
--- /dev/null
+++ b/cayenne/src/test/java/org/apache/cayenne/access/translator/select/DescriptorColumnExtractorVerticalInheritanceIT.java
@@ -0,0 +1,90 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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
+ *
+ *    https://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.apache.cayenne.access.translator.select;
+
+import java.util.Set;
+
+import org.apache.cayenne.dba.DbAdapter;
+import org.apache.cayenne.map.EntityResolver;
+import org.apache.cayenne.query.FluentSelect;
+import org.apache.cayenne.query.ObjectSelect;
+import org.apache.cayenne.reflect.ClassDescriptor;
+import org.apache.cayenne.testdo.inheritance_vertical.IvRoot;
+import org.apache.cayenne.testdo.inheritance_vertical.IvSub3;
+import org.apache.cayenne.unit.CayenneProjects;
+import org.apache.cayenne.unit.CayenneTestsEnv;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.mockito.Mockito;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Verifies that {@link DescriptorColumnExtractor} registers exactly one {@code EntityResult} field per column,
+ * under a unique key.
+ * <p>
+ * See CAY-2911.
+ */
+public class DescriptorColumnExtractorVerticalInheritanceIT {
+
+    @RegisterExtension
+    static final CayenneTestsEnv env = CayenneTestsEnv.forProject(CayenneProjects.INHERITANCE_VERTICAL_PROJECT);
+
+    private SelectTranslatorContext extract(FluentSelect<?, ?> query, String entityName) {
+        EntityResolver resolver = env.context().getEntityResolver();
+        ClassDescriptor classDescriptor = resolver.getClassDescriptor(entityName);
+
+        SelectTranslatorContext translatorContext =
+                new SelectTranslatorContext(query, Mockito.mock(DbAdapter.class), resolver, null);
+
+        new DescriptorColumnExtractor(translatorContext, classDescriptor).extract();
+        return translatorContext;
+    }
+
+    private void assertFieldCountMatchesColumnCount(FluentSelect<?, ?> query, String entityName) {
+        SelectTranslatorContext context = extract(query, entityName);
+        EntityResolver resolver = env.context().getEntityResolver();
+
+        assertEquals(context.getResultNodeList().size(),
+                context.getRootEntityResult().getDbFields(resolver).size(),
+                "entity result field count must match the number of emitted columns");
+    }
+
+    @Test
+    public void fieldCountMatchesColumnCountForInheritanceRoot() {
+        assertFieldCountMatchesColumnCount(ObjectSelect.query(IvRoot.class).column(IvRoot.SELF), "IvRoot");
+    }
+
+    @Test
+    public void fieldCountMatchesColumnCountForSub3() {
+        assertFieldCountMatchesColumnCount(ObjectSelect.query(IvSub3.class).column(IvSub3.SELF), "IvSub3");
+    }
+
+    @Test
+    public void flattenedToOneFieldsArePathQualified() {
+        SelectTranslatorContext context = extract(ObjectSelect.query(IvSub3.class).column(IvSub3.SELF), "IvSub3");
+        EntityResolver resolver = env.context().getEntityResolver();
+
+        Set<String> keys = context.getRootEntityResult().getDbFields(resolver).keySet();
+
+        assertTrue(keys.contains("sub3.ID"), () -> "expected path-qualified 'sub3.ID' in " + keys);
+        assertTrue(keys.contains("sub3.IV_ROOT_ID"), () -> "expected path-qualified 'sub3.IV_ROOT_ID' in " + keys);
+    }
+}