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 e33e741ec3291337ddb686f4c8bfa562d0c9c587
Author: Andrus Adamchik <[email protected]>
AuthorDate: Mon Jul 20 15:52:21 2026 +0200

    CAY-2980 Improve model name generation
    
    auto-renaming properties conflicting with Persistent object
---
 RELEASE-NOTES.txt                                  |  1 +
 ai-plugin/references/model-naming-conventions.md   | 14 +++++++----
 ai-plugin/skills/cayenne-model-naming/SKILL.md     | 11 +++++----
 .../dbsync/naming/BaseObjectNameGenerator.java     | 23 ++++++++++++++++--
 .../naming/DefaultObjectNameGeneratorTest.java     | 28 ++++++++++++++++++++++
 .../project/validation/NameValidationHelper.java   | 25 ++++++++++---------
 6 files changed, 77 insertions(+), 25 deletions(-)

diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt
index 1dcfd71c4..dfdb33ca7 100644
--- a/RELEASE-NOTES.txt
+++ b/RELEASE-NOTES.txt
@@ -27,6 +27,7 @@ CAY-2974 CayenneSqlException with a reference to translated 
query
 CAY-2975 Mnemonic table aliases in generated SQL
 CAY-2978 AI skill: "cayenne-model-naming"
 CAY-2979 AI skill: "cayenne-full-db-sync"
+CAY-2980 Improve model name generation
 
 Bug Fixes:
 
diff --git a/ai-plugin/references/model-naming-conventions.md 
b/ai-plugin/references/model-naming-conventions.md
index 32aec7471..c65b83005 100644
--- a/ai-plugin/references/model-naming-conventions.md
+++ b/ai-plugin/references/model-naming-conventions.md
@@ -143,11 +143,15 @@ The three cases above don't exhaust the ways a purely 
mechanical transliteration
 you spot a name where a human reading the underlying DB name would obviously 
do better, and the fix
 is defensible (not a guess), apply the same conservative treatment. Some more 
examples:
 
-- **Genuinely illegal identifiers** the generator passed through — a name 
starting with a digit, or
-  literally `class`, whose getter `getClass()` collides with the final 
`Object.getClass()`. Java
-  *keywords* are **not** a problem — `default`, `package`, `return` and the 
like compile fine, since
-  class generation prefixes the field/parameter name with `_` and embeds the 
capitalized name in the
-  accessors (`getDefault()` / `setDefault()`). Leave keyword-named properties 
alone.
+- **Genuinely illegal identifiers** the generator passed through — chiefly a 
name starting with a
+  digit. Java *keywords* are **not** a problem — `default`, `package`, 
`return` and the like compile
+  fine, since class generation prefixes the field/parameter name with `_` and 
embeds the capitalized
+  name in the accessors (`getDefault()` / `setDefault()`); leave keyword-named 
properties alone.
+  Property names clashing with base-class getters (`class`, `objectId`, 
`objectContext`,
+  `persistenceState`, `snapshotVersion` — e.g. `getClass()` collides with the 
final
+  `Object.getClass()`) are qualified with the entity name by the generator 
itself (`class` on
+  `student` → `studentClass`) — already handled; models imported by older 
Cayenne versions may still
+  carry them raw, and those do need the same entity-qualified rename.
 - **Lost acronym casing** — `HTTPURL` → `Gametype`-style collapse loses the 
acronym; `httpUrl` /
   `url` may read better than `httpurl`.
 - **Plural table → singular entity** — a `CUSTOMERS` table yields `Customers`; 
an entity is a single
diff --git a/ai-plugin/skills/cayenne-model-naming/SKILL.md 
b/ai-plugin/skills/cayenne-model-naming/SKILL.md
index 44b1af552..fb26687d9 100644
--- a/ai-plugin/skills/cayenne-model-naming/SKILL.md
+++ b/ai-plugin/skills/cayenne-model-naming/SKILL.md
@@ -95,11 +95,12 @@ correct — leave them.** Flag only the cases the 
deterministic generator can't
    names (`customer`, `orders`); a relationship is a role/property, and the 
prefix is noise there.
    **Leave the entity names alone** — the prefix on classes is the user's 
choice, and renaming
    entities regenerates classes.
-4. **Other clear, defensible improvements** — illegal identifiers 
(digit-leading names, or `class`,
-   whose getter would clash with `Object.getClass()` — Java keywords are fine, 
cgen escapes them),
-   lost acronym casing, obvious cryptic abbreviations applied consistently,
-   plural-table-to-singular-entity. Conservative by default; when unsure, 
leave the baseline name
-   and ask.
+4. **Other clear, defensible improvements** — illegal identifiers 
(digit-leading names; Java
+   keywords are fine — cgen escapes them — and properties clashing with 
base-class getters like
+   `class` / `objectId` are auto-qualified with the entity name by the 
generator, though older
+   imports may carry them raw), lost acronym casing, obvious cryptic 
abbreviations applied
+   consistently, plural-table-to-singular-entity. Conservative by default; 
when unsure, leave the
+   baseline name and ask.
 
 Relationship cleanup is anchored on the **DbRelationship** — it's the 
first-class citizen, since
 every FK has one whether or not an ObjRelationship was generated on top. The 
rules above (run-together,
diff --git 
a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/naming/BaseObjectNameGenerator.java
 
b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/naming/BaseObjectNameGenerator.java
index 26d230ade..b0af9b546 100644
--- 
a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/naming/BaseObjectNameGenerator.java
+++ 
b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/naming/BaseObjectNameGenerator.java
@@ -22,6 +22,7 @@ import org.apache.cayenne.map.DbAttribute;
 import org.apache.cayenne.map.DbEntity;
 import org.apache.cayenne.map.DbJoin;
 import org.apache.cayenne.map.DbRelationship;
+import org.apache.cayenne.project.validation.NameValidationHelper;
 import org.apache.cayenne.util.Util;
 
 import java.util.List;
@@ -43,7 +44,9 @@ public abstract class BaseObjectNameGenerator implements 
ObjectNameGenerator {
 
     @Override
     public String objAttributeName(DbAttribute dbAttribute) {
-        return Util.underscoredToJava(dbAttribute.getName(), false);
+        String name = Util.underscoredToJava(dbAttribute.getName(), false);
+        String entityName = dbAttribute.getEntity() != null ? 
dbAttribute.getEntity().getName() : null;
+        return fixPersistentBaseProperty(name, entityName);
     }
 
     @Override
@@ -72,7 +75,23 @@ public abstract class BaseObjectNameGenerator implements 
ObjectNameGenerator {
         String baseName = toMany
                 ? toManyBase(joins, targetEntityName)
                 : toOneBase(joins, targetEntityName);
-        return Util.underscoredToJava(baseName, false);
+        String name = Util.underscoredToJava(baseName, false);
+
+        DbEntity sourceEntity = 
joins.getFirst().getRelationship().getSourceEntity();
+        return fixPersistentBaseProperty(name, sourceEntity != null ? 
sourceEntity.getName() : null);
+    }
+
+    /**
+     * Qualifies a property name that has a getter in Object or 
PersistentObject with the entity name, the way
+     * a human would ("class" on "student" becomes "studentClass") — such a 
name can't be generated as-is.
+     */
+    private String fixPersistentBaseProperty(String name, String entityName) {
+
+        if (entityName == null || 
!NameValidationHelper.getInstance().invalidPersistentObjectProperty(name)) {
+            return name;
+        }
+
+        return Util.underscoredToJava(dbEntityBaseName(entityName) + "_" + 
name, false);
     }
 
     protected boolean isToMany(DbRelationship... relationshipChain) {
diff --git 
a/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/naming/DefaultObjectNameGeneratorTest.java
 
b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/naming/DefaultObjectNameGeneratorTest.java
index 214573df7..35a24568d 100644
--- 
a/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/naming/DefaultObjectNameGeneratorTest.java
+++ 
b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/naming/DefaultObjectNameGeneratorTest.java
@@ -18,6 +18,8 @@
  ****************************************************************/
 package org.apache.cayenne.dbsync.naming;
 
+import java.sql.Types;
+
 import org.apache.cayenne.map.DbAttribute;
 import org.apache.cayenne.map.DbEntity;
 import org.apache.cayenne.map.DbJoin;
@@ -260,4 +262,30 @@ public class DefaultObjectNameGeneratorTest {
         assertEquals("name", generator.objAttributeName(new 
DbAttribute("NAME")));
         assertEquals("artistName", generator.objAttributeName(new 
DbAttribute("ARTIST_NAME")));
     }
+
+    @Test
+    public void objAttributeName_PersistentBaseProperty() {
+
+        // names with a getter in Object or PersistentObject are qualified 
with the entity name
+        DbEntity student = new DbEntity("STUDENT");
+        assertEquals("studentClass", generator.objAttributeName(new 
DbAttribute("CLASS", Types.VARCHAR, student)));
+        assertEquals("studentObjectId",
+                generator.objAttributeName(new DbAttribute("OBJECT_ID", 
Types.INTEGER, student)));
+        assertEquals("studentSnapshotVersion",
+                generator.objAttributeName(new DbAttribute("SNAPSHOT_VERSION", 
Types.INTEGER, student)));
+
+        // ordinary names are unaffected
+        assertEquals("classRoom", generator.objAttributeName(new 
DbAttribute("CLASS_ROOM", Types.VARCHAR, student)));
+    }
+
+    @Test
+    public void dbRelationshipName_ToOne_PersistentBaseProperty() {
+
+        // an FK-derived name with a getter in Object or PersistentObject is 
qualified with the source entity name
+        DbRelationship r1 = makeRelationship("student", "class_id", "class", 
"id", false);
+        assertEquals("studentClass", dbRelationshipName(r1));
+
+        DbRelationship r2 = makeRelationship("AUDIT", "OBJECT_CONTEXT_ID", 
"CONTEXT", "ID", false);
+        assertEquals("auditObjectContext", dbRelationshipName(r2));
+    }
 }
diff --git 
a/cayenne-project/src/main/java/org/apache/cayenne/project/validation/NameValidationHelper.java
 
b/cayenne-project/src/main/java/org/apache/cayenne/project/validation/NameValidationHelper.java
index c46b345d7..427235815 100644
--- 
a/cayenne-project/src/main/java/org/apache/cayenne/project/validation/NameValidationHelper.java
+++ 
b/cayenne-project/src/main/java/org/apache/cayenne/project/validation/NameValidationHelper.java
@@ -82,18 +82,11 @@ public class NameValidationHelper {
             "package",
             "synchronized");
 
-    public boolean isReservedJavaKeyword(String word) {
-        return RESERVED_JAVA_KEYWORDS.contains(word);
-    }
-
-    // a property is considered invalid if there is a getter or a setter for 
it in
-    // java.lang.Object or PersistentObject
+    // property getter or setter would conflict with Object or Persistent
     private static final Collection<String> PERSISTENT_BASE_PROPERTIES = 
List.of(
             "class",
-            "committedSnapshot",
-            "currentSnapshot",
-            "dataContext",
             "objectId",
+            "objectContext",
             "persistenceState",
             "snapshotVersion");
 
@@ -106,6 +99,10 @@ public class NameValidationHelper {
         return sharedInstance;
     }
 
+    public boolean isReservedJavaKeyword(String word) {
+        return RESERVED_JAVA_KEYWORDS.contains(word);
+    }
+
     /**
      * This is more of a sanity check than a real validation. As different DBs 
allow
      * different chars in identifiers, here we simply check for dots.
@@ -119,7 +116,7 @@ public class NameValidationHelper {
      */
     public String invalidCharsInObjPathComponent(String objPathComponent) {
         String invalidChars = validateJavaIdentifier(objPathComponent, "");
-        return (invalidChars.length() > 0) ? invalidChars : null;
+        return (!invalidChars.isEmpty()) ? invalidChars : null;
     }
 
     public String invalidCharsInJavaClassName(String javaClassName) {
@@ -134,7 +131,7 @@ public class NameValidationHelper {
             invalidChars = validateJavaIdentifier(toks.nextToken(), 
invalidChars);
         }
 
-        return (invalidChars.length() > 0) ? invalidChars : null;
+        return !invalidChars.isEmpty() ? invalidChars : null;
     }
 
     public boolean invalidPersistentObjectClass(String 
persistentObjectClassFQN) {
@@ -167,14 +164,16 @@ public class NameValidationHelper {
             }
         }
 
+        StringBuilder buf = new StringBuilder(invalidChars);
         for (int i = 1; i < len; i++) {
 
             if (!Character.isJavaIdentifierPart(id.charAt(i))) {
-                if (invalidChars.indexOf(id.charAt(i)) < 0) {
-                    invalidChars = invalidChars + id.charAt(i);
+                if (buf.toString().indexOf(id.charAt(i)) < 0) {
+                    buf.append(id.charAt(i));
                 }
             }
         }
+        invalidChars = buf.toString();
 
         return invalidChars;
     }

Reply via email to