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; }
