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 84ad32f69d605a75c3c6f03f2bf429abe3e98a72 Author: Andrus Adamchik <[email protected]> AuthorDate: Mon Jul 20 15:52:21 2026 +0200 CAY-2980 Improve model name generation to-many generated names should attempt to strip shared prefix between entity names --- ai-plugin/references/model-naming-conventions.md | 17 ++++----- ai-plugin/skills/cayenne-model-naming/SKILL.md | 14 +++++--- .../dbsync/naming/BaseObjectNameGenerator.java | 31 ++++++++++++++++- .../naming/DefaultObjectNameGeneratorTest.java | 40 +++++++++++++++++++--- .../cayenne/dbsync/reverse/dbload/DbLoaderIT.java | 3 +- 5 files changed, 86 insertions(+), 19 deletions(-) diff --git a/ai-plugin/references/model-naming-conventions.md b/ai-plugin/references/model-naming-conventions.md index 66a19e839..94df7ea39 100644 --- a/ai-plugin/references/model-naming-conventions.md +++ b/ai-plugin/references/model-naming-conventions.md @@ -41,7 +41,7 @@ Concretely, the generator produces: | `db-entity` name | stem, split on `_`, capitalize each token | `ARTIST_GROUP` → `ArtistGroup` | | `db-attribute` name | split on `_`, camelCase | `FIRST_NAME` → `firstName` | | to-one relationship | FK column, minus its trailing `_ID`/`ID` when present; target entity name for a compound (multi-column) FK or when there are no joins | `MANAGER_ID` → `manager`; `BIRTH_COUNTRY` → `birthCountry` | -| to-many relationship | English plural of the target entity name, prefixed with the FK role qualifier when the FK column embeds the source entity name | `PAINTING` → `paintings`; `HOME_TEAM_ID` → `homeGames` | +| to-many relationship | English plural of the target entity name (minus the leading `_`-tokens shared with the source table name), prefixed with the FK role qualifier when the FK column embeds the source entity name | `PAINTING` → `paintings`; `HOME_TEAM_ID` → `homeGames`; `AA_TEAM` referencing `AA_GAME` → `games` | | name collision within an entity | append a numeric suffix | `team`, `team1`, `team2` … | Generation also collapses all-upper tokens to lowercase and preserves already-mixed @@ -84,8 +84,8 @@ directions are not equally affected** — the to-one and to-many sides are named - **to-many side collides when the FK doesn't embed the source entity name.** When the FK column is `<ROLE>_<SOURCE_ENTITY>[_ID]`, the generator prepends the role qualifier to the pluralized target (`HOME_TEAM_ID` / `AWAY_TEAM_ID` on FKs to `TEAM` → `homeGames` / `awayGames`) — those are already - correct, leave them. The entity-name part may drop a common table prefix: `home_team_id` - referencing `aa_team` still yields `homeAaGames`. It still collides when the FK carries a role + correct, leave them. The FK column may drop a common table prefix: `home_team_id` referencing + `aa_team` still yields the `home` qualifier. It still collides when the FK carries a role unrelated to the source entity name: `MANAGER_ID` / `AUDITOR_ID` FKs to `EMPLOYEE` both produce `projects` / `projects1` on `Employee`, because the role can't be mechanically tied to the entity. Name each collection by its @@ -121,12 +121,13 @@ cleanly *when it's used*: entity names come through stripped (`AA_CUSTOMER` → no problem — leave that model alone. The case that needs you is when the prefix is **kept on the entity names** (stripping was not -configured — often intentional, treating the prefix as a class-name namespace). Then the prefix -**leaks into the relationship names**, which you almost never want: +configured — often intentional, treating the prefix as a class-name namespace). The generator keeps +the prefix out of **to-many** names on its own: the leading `_`-tokens the source and target table +names share are dropped from the pluralized target (`aa_customer` referencing `aa_order` → +`orders`) — those are already clean. What still **leaks**: -- to-many names are the pluralized target entity name; with the prefix kept, the target's prefixed - name flows straight in → `aaOrders`, `aaCustomers`. -- to-one names built from a prefixed FK column (`AA_CUSTOMER_ID`) carry it too → `aaCustomer`. +- to-one names built from a prefixed FK column (`AA_CUSTOMER_ID`) carry it → `aaCustomer`. +- models imported by **older Cayenne versions** kept the prefix in to-many names too → `aaOrders`. A relationship name is a **role/property** on a class (`order.getAaCustomer()`), and the shared prefix is pure noise there. **Strip the common prefix from the relationship names** — `aaOrders` → diff --git a/ai-plugin/skills/cayenne-model-naming/SKILL.md b/ai-plugin/skills/cayenne-model-naming/SKILL.md index edd50b59d..b86aa44d7 100644 --- a/ai-plugin/skills/cayenne-model-naming/SKILL.md +++ b/ai-plugin/skills/cayenne-model-naming/SKILL.md @@ -82,15 +82,19 @@ correct — leave them.** Flag only the cases the deterministic generator can't names are FK-based even without an `_ID` suffix (`HOME_TEAM_ID` → `homeTeam`, `BIRTH_COUNTRY` → `birthCountry`), and to-many names pick up a FK role qualifier when the FK embeds the source entity name, even one sans a common table prefix (`HOME_TEAM_ID` → `homeGames`; `home_team_id` - referencing `aa_team` → `homeAaGames`). What still collides: to-many collections whose FK role is + referencing `aa_team` → `homeGames` too). What still collides: to-many collections whose FK role is unrelated to the entity name (`MANAGER_ID` / `AUDITOR_ID` → `projects` / `projects1` — rename by role: `managedProjects` / `auditedProjects`), and — in models imported by older Cayenne versions — to-one ends whose FK columns lack an `_ID` suffix and collapsed to `employee` / `employee1`. 3. **A common entity prefix leaking into relationship names** — when every entity shares a prefix - that was **kept** on the class names (`AaCustomer`, `AaOrder`), the relationship names inherit it - (`aaOrders`, `aaCustomer`). Strip the prefix from the relationship names (`orders`, `customer`); - 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. + that was **kept** on the class names (`AaCustomer`, `AaOrder`), the prefix can leak into + relationship names. The generator already keeps it out of to-many names (the leading `_`-tokens + shared by the source and target table names are dropped → `orders`, not `aaOrders`); what leaks is + to-one names built from prefixed FK columns (`AA_CUSTOMER_ID` → `aaCustomer`) and to-many names in + models imported by older Cayenne versions (`aaOrders`). Strip the prefix from those relationship + 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** — reserved words, lost acronym casing, obvious cryptic abbreviations applied consistently, plural-table-to-singular-entity. Conservative by default; when unsure, leave the baseline name and ask. 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 e03e956a8..26d230ade 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 @@ -87,10 +87,39 @@ public abstract class BaseObjectNameGenerator implements ObjectNameGenerator { } protected String toManyBase(List<DbJoin> joins, String targetEntityName) { - String plural = EnglishInflector.pluralOf(dbEntityBaseName(targetEntityName).toLowerCase()); + + String targetBase = dbEntityBaseName(targetEntityName); + + DbEntity sourceEntity = joins.isEmpty() ? null : joins.getFirst().getRelationship().getSourceEntity(); + if (sourceEntity != null) { + targetBase = stripSharedPrefix(dbEntityBaseName(sourceEntity.getName()), targetBase); + } + + String plural = EnglishInflector.pluralOf(targetBase.toLowerCase()); String qualifier = toManyRoleQualifier(joins); return qualifier != null ? qualifier + "_" + plural : plural; } + + private static String stripSharedPrefix(String sourceName, String targetName) { + + String[] sourceTokens = sourceName.split("_"); + String[] targetTokens = targetName.split("_"); + + // strip every leading "_"-token shared by both names... + int maxShared = Math.min(sourceTokens.length, targetTokens.length); + int strip = 0; + for (int i = 0; i < maxShared && sourceTokens[i].equalsIgnoreCase(targetTokens[i]); i++) { + int next = strip + targetTokens[i].length() + 1; + + // ... but stop before the base name degenerates to a single letter or less + if (targetName.length() - next <= 1) { + break; + } + strip = next; + } + + return targetName.substring(strip); + } private String toManyRoleQualifier(List<DbJoin> joins) { 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 5ec86462e..214573df7 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 @@ -185,17 +185,49 @@ public class DefaultObjectNameGeneratorTest { // FK columns matching a "_"-token suffix of the entity name still carry the role... DbRelationship r1 = makeRelationship("acme_team", "id", "acme_game", "home_team_id", true); - assertEquals("homeAcmeGames", dbRelationshipName(r1)); + assertEquals("homeGames", dbRelationshipName(r1)); DbRelationship r2 = makeRelationship("acme_team", "id", "acme_game", "visiting_team_id", true); - assertEquals("visitingAcmeGames", dbRelationshipName(r2)); + assertEquals("visitingGames", dbRelationshipName(r2)); // ... and a plain suffix reference gets no qualifier DbRelationship r3 = makeRelationship("acme_team", "id", "acme_award", "team_id", true); - assertEquals("acmeAwards", dbRelationshipName(r3)); + assertEquals("awards", dbRelationshipName(r3)); DbRelationship r4 = makeRelationship("acme_game_type", "id", "acme_game", "type_id", true); - assertEquals("acmeGames", dbRelationshipName(r4)); + assertEquals("games", dbRelationshipName(r4)); + } + + @Test + public void dbRelationshipName_ToMany_SharedTablePrefix() { + + // a leading "_"-token shared by both table names is schema vocabulary, not a part of the role, + // and is stripped to mirror the FK-based to-one names that never carry it + DbRelationship r1 = makeRelationship("acme_team", "id", "acme_game", "team_id", true); + assertEquals("games", dbRelationshipName(r1)); + + DbRelationship r2 = makeRelationship("acme_country", "id", "acme_draft_pick", "drafted_by_country_id", true); + assertEquals("draftedByDraftPicks", dbRelationshipName(r2)); + + // all shared tokens are stripped, including a source name that is a full token-prefix of the target + DbRelationship r3 = makeRelationship("acme_playoff_series", "id", "acme_playoff_series_game", + "playoff_series_id", true); + assertEquals("games", dbRelationshipName(r3)); + + DbRelationship r4 = makeRelationship("aa_bb_team", "id", "aa_bb_game", "team_id", true); + assertEquals("games", dbRelationshipName(r4)); + + // a prefix on the target alone is kept... + DbRelationship r5 = makeRelationship("team", "id", "acme_game", "team_id", true); + assertEquals("acmeGames", dbRelationshipName(r5)); + + // ... as are differing prefixes + DbRelationship r6 = makeRelationship("aa_team", "id", "bb_game", "team_id", true); + assertEquals("bbGames", dbRelationshipName(r6)); + + // stripping backs off when the base name would shrink to a single letter + DbRelationship r7 = makeRelationship("acme_team", "id", "acme_a", "team_id", true); + assertEquals("acmeAs", dbRelationshipName(r7)); } @Test diff --git a/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/reverse/dbload/DbLoaderIT.java b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/reverse/dbload/DbLoaderIT.java index d73c0fb5b..e30b0e73e 100644 --- a/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/reverse/dbload/DbLoaderIT.java +++ b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/reverse/dbload/DbLoaderIT.java @@ -117,7 +117,8 @@ public class DbLoaderIT { // DbRelationship assertEquals(5, artist.getRelationships().size()); - DbRelationship exhibits = artist.getRelationship("artistExhibits"); + // the "artist" prefix shared with the source table is stripped from the relationship name + DbRelationship exhibits = artist.getRelationship("exhibits"); assertNotNull(exhibits); assertEquals("ARTIST_EXHIBIT", exhibits.getTargetEntityName().toUpperCase()); DbEntity target = exhibits.getTargetEntity();
