Skip to content

Commit 1797ede

Browse files
committed
CAY-2978 AI skill: "cayenne-model-naming"
1 parent b4e28e6 commit 1797ede

7 files changed

Lines changed: 429 additions & 0 deletions

File tree

RELEASE-NOTES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ CAY-2971 Remove extra spaces within SQL parenthesis
2525
CAY-2972 Fewer parentheses in generated SQL
2626
CAY-2974 CayenneSqlException with a reference to translated query
2727
CAY-2975 Mnemonic table aliases in generated SQL
28+
CAY-2978 AI skill: "cayenne-model-naming"
2829

2930
Bug Fixes:
3031

ai-plugin/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ This plugin teaches Claude how to:
2424

2525
- Edit Cayenne DataMap (`*.map.xml`) and project descriptor (`cayenne-*.xml`) files a-la-carte (add entities, relationships, queries, embeddables).
2626
- Reverse-engineer a database schema into a DataMap by driving CayenneModeler.
27+
- Polish the reverse-engineered Object-layer names (entities, attributes, relationships) into idiomatic Java, fixing the few cases the deterministic name generator can't.
2728
- Regenerate Java entity classes from a DataMap.
2829
- Bootstrap `CayenneRuntime` in a Java application and write `ObjectSelect` / `SQLSelect` queries.
2930

@@ -59,6 +60,7 @@ ai-plugin/
5960
├── skills/ # auto-triggering workflows
6061
│ ├── cayenne-modeling/ # edit *.map.xml and cayenne-*.xml
6162
│ ├── cayenne-db-import/ # import a DB schema (Modeler GUI)
63+
│ ├── cayenne-model-naming/ # polish Obj-layer names after import / on request
6264
│ ├── cayenne-cgen/ # regenerate Java classes via MCP
6365
│ ├── cayenne-modeler/ # open the GUI on a project
6466
│ ├── cayenne-runtime/ # bootstrap CayenneRuntime in an app
@@ -68,6 +70,8 @@ ai-plugin/
6870
├── datamap-schema.md
6971
├── project-descriptor-schema.md
7072
├── dbimport-config.md
73+
├── model-naming-conventions.md
74+
├── model-naming-rename-safety.md
7175
├── cgen-config.md
7276
├── runtime-api.md
7377
├── query-api.md
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
<!--
2+
Licensed to the Apache Software Foundation (ASF) under one
3+
or more contributor license agreements. See the NOTICE file
4+
distributed with this work for additional information
5+
regarding copyright ownership. The ASF licenses this file
6+
to you under the Apache License, Version 2.0 (the
7+
"License"); you may not use this file except in compliance
8+
with the License. You may obtain a copy of the License at
9+
10+
https://www.apache.org/licenses/LICENSE-2.0
11+
12+
Unless required by applicable law or agreed to in writing,
13+
software distributed under the License is distributed on an
14+
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
KIND, either express or implied. See the License for the
16+
specific language governing permissions and limitations
17+
under the License.
18+
-->
19+
# Obj-layer naming conventions — what to fix, what to leave alone
20+
21+
Reference for the `cayenne-model-naming` skill. The governing rule: **the Modeler already
22+
generates good names for the common case. This is a polish pass, not a rewrite.** Read the
23+
"deterministic baseline" first so you can recognize a name that is already correct and skip it.
24+
25+
## The deterministic baseline (leave these names alone)
26+
27+
Reverse engineering (`dbimport_run`, the Modeler's DB Import) generates the Obj-layer names before
28+
you ever see the model, then de-duplicates any collisions. It already applies these principles — and
29+
you must preserve them:
30+
31+
1. **Obj names stay as close to the DB names as possible** — the object name is a transliteration
32+
of the table/column, not a re-invention.
33+
2. **Java identifier / class conventions** — classes PascalCase, properties camelCase.
34+
3. **snake_case → camelCase / PascalCase** — split on `_`, drop the underscores, camel-join.
35+
4. **Relationship names from entity name + cardinality** — see below.
36+
37+
Concretely, the generator produces:
38+
39+
| DB element | Rule | Result |
40+
|---|---|---|
41+
| `db-entity` name | stem, split on `_`, capitalize each token | `ARTIST_GROUP``ArtistGroup` |
42+
| `db-attribute` name | split on `_`, camelCase | `FIRST_NAME``firstName` |
43+
| to-one relationship | FK column minus trailing `_ID`/`ID`; else target entity name | `MANAGER_ID``manager` |
44+
| to-many relationship | English plural of the target entity name | `PAINTING``paintings` |
45+
| name collision within an entity | append a numeric suffix | `team`, `team1`, `team2`|
46+
47+
Generation also collapses all-upper tokens to lowercase and preserves already-mixed
48+
case. So `GameType`, `gameType`, `firstName`, `ArtistGroup`, `paintings`, `manager` are **all
49+
already correct** — do not re-case, re-spell, re-pluralize, or "prettify" them. If a name is a
50+
clean camelCase/PascalCase transliteration of its DB element with the right cardinality, it is
51+
done. Touch nothing.
52+
53+
## Where the deterministic algorithm falls short — the AI job
54+
55+
The cases below are the ones we've identified where the generator can't do better and your judgment
56+
adds real value. They are **illustrative, not exhaustive** — the generator is a deterministic
57+
transliteration, so any place where a *human* reading the DB name would produce a clearly better
58+
Java name than a mechanical `_`-split is fair game (§5). Focus your attention on these gaps; don't
59+
touch names the baseline already got right.
60+
61+
### 1. Run-together DB names with no separators
62+
63+
Word-splitting happens **only on `_`**. A single-token, uniform-case DB name has no boundary to
64+
split on, so a multi-word concept collapses into one lowercased chunk. This is the classic case:
65+
66+
| DB name | Generator output | Correct |
67+
|---|---|---|
68+
| `gametype` / `GAMETYPE` | `Gametype` | `GameType` |
69+
| `dateofbirth` | `Dateofbirth` | `dateOfBirth` |
70+
| `ordernumber` | `Ordernumber` | `orderNumber` |
71+
| `custaddr` | `Custaddr` | `CustomerAddress` (with expansion — see §3) |
72+
73+
Split on the **real** word boundary, using domain knowledge, and re-apply the Java convention
74+
(PascalCase for entities, camelCase for attributes and relationships). Only split where you are confident a boundary
75+
exists — never invent one (`status` is not `sta` + `tus`; `metadata` is one word). Names that are
76+
**already** mixed-case (`GameType`, `gameType`) were handled by the generator — leave them.
77+
78+
### 2. More than one relationship between the same two tables
79+
80+
When two FKs point at the same target table (or two relationships otherwise share a target),
81+
generation can't invent a role, so it disambiguates with numbers. But **the two
82+
directions are not equally affected** — the to-one and to-many sides are named by different rules:
83+
84+
- **to-many side always collides.** To-many naming ignores the FK column entirely and always uses
85+
the pluralized target entity name, so two relationships to the same target both become e.g.
86+
`games` / `games1` (or `people` / `people1`) no matter how well the FKs are named. This is the
87+
common case and the main reason this rule exists. Name each collection by the **logically opposite**
88+
role instead: on `Team`, the two reverse collections of `Game` become `homeGames` / `awayGames`.
89+
90+
- **to-one side usually does NOT collide.** To-one naming is FK-column-based — it strips a trailing
91+
`_ID`/`ID`, so distinct `*_ID` FKs already yield distinct, good names (`HOME_TEAM_ID``homeTeam`,
92+
`AWAY_TEAM_ID``awayTeam`). Leave those alone. It **only** collides in the fallback path: when a
93+
FK column does *not* end in `ID`/`_ID` (or is null / has no joins), the generator drops to the
94+
target entity name, so two such FKs both become e.g. `employee` / `employee1`. There, derive the
95+
role from the FK column yourself even though it lacks the `_ID` suffix (`MANAGER``manager`,
96+
`SUPERVISOR``supervisor`).
97+
98+
So in the typical "two well-named `*_ID` FKs" model you'll rename **only the to-many collections**
99+
(`games`/`games1`), and the to-one ends are already fine. When you do rename both ends, give them
100+
matching opposite-role names so the pair is legible from either side (`homeTeam``homeGames`,
101+
`awayTeam``awayGames`).
102+
103+
### 3. Genuinely cryptic abbreviations (secondary, be conservative)
104+
105+
Expand an abbreviation only when the win is clear **and** you apply it consistently across the whole
106+
model: `qty``quantity`, `amt``amount`, `dob``dateOfBirth`. An unfamiliar or ambiguous
107+
abbreviation stays as-is (case-normalized) rather than becoming a wrong guess. If the model is full
108+
of domain-specific abbreviations, ask the user for a glossary instead of guessing element by element.
109+
110+
### 4. A common entity prefix leaking into relationship names
111+
112+
Many schemas tag every table with the same prefix (`AA_CUSTOMER`, `AA_ORDER`, or `os_t1`, `os_t2`).
113+
The reverse-engineering **"Strip from Table Names"** (`stripFromTableNames`) setting handles this
114+
cleanly *when it's used*: entity names come through stripped (`AA_CUSTOMER``Customer`) and there's
115+
no problem — leave that model alone.
116+
117+
The case that needs you is when the prefix is **kept on the entity names** (stripping was not
118+
configured — often intentional, treating the prefix as a class-name namespace). Then the prefix
119+
**leaks into the relationship names**, which you almost never want:
120+
121+
- to-many names are the pluralized target entity name; with the prefix kept, the target's prefixed
122+
name flows straight in → `aaOrders`, `aaCustomers`.
123+
- to-one names built from a prefixed FK column (`AA_CUSTOMER_ID`) carry it too → `aaCustomer`.
124+
125+
A relationship name is a **role/property** on a class (`order.getAaCustomer()`), and the shared
126+
prefix is pure noise there. **Strip the common prefix from the relationship names**`aaOrders`
127+
`orders`, `aaCustomer``customer` — and mirror the change onto the paired DbRelationship.
128+
129+
**Leave the ObjEntity names as they are.** The prefix on the class names is the user's choice (if
130+
they'd wanted it gone from entities they would have set `stripFromTableNames`); renaming entities is
131+
a bigger, class-regenerating change. This case cleans relationship names only.
132+
133+
### 5. Other cases — use judgment
134+
135+
The three cases above don't exhaust the ways a purely mechanical transliteration can miss. Whenever
136+
you spot a name where a human reading the underlying DB name would obviously do better, and the fix
137+
is defensible (not a guess), apply the same conservative treatment. Some more examples:
138+
139+
- **Reserved words / illegal identifiers** the generator passed through — a column literally named
140+
`class`, `package`, `default`, or one starting with a digit needs a legal Java name.
141+
- **Lost acronym casing**`HTTPURL``Gametype`-style collapse loses the acronym; `httpUrl` /
142+
`url` may read better than `httpurl`.
143+
- **Plural table → singular entity** — a `CUSTOMERS` table yields `Customers`; an entity is a single
144+
row, so `Customer` is usually the intent (be careful: only when clearly a pluralized table name,
145+
and check for a resulting collision).
146+
- **Redundant entity-name prefix on an attribute**`Artist.artistName``name` — only when it's
147+
clearly noise and doesn't collide.
148+
149+
The bar is the same throughout: a clear, defensible improvement over the mechanical output, applied
150+
consistently. When in doubt, leave the baseline name and surface the question to the user rather than
151+
guessing.
152+
153+
## Target forms (for the names you actually change)
154+
155+
- **ObjEntity `name`** — PascalCase, singular preferred; keep it equal to the `className` simple name.
156+
- **ObjAttribute `name`** — camelCase; strip type/Hungarian prefixes (`strName``name`, `n_count`
157+
`count`) only when unambiguous.
158+
- **ObjRelationship, to-one** — singular camelCase role.
159+
- **ObjRelationship, to-many** — plural camelCase.
160+
- **DbRelationship `name`** — mirror the paired ObjRelationship name (see below).
161+
162+
## DbRelationship names
163+
164+
A DbRelationship name is **arbitrary** — there is no DB metadata behind it (unlike a DbEntity/
165+
DbAttribute, which mirror a real table/column). The working convention is that a DbRelationship's
166+
name matches the ObjRelationship built on it, **per direction**. So when you rename a to-one
167+
ObjRelationship to `homeTeam`, rename its backing single-hop DbRelationship to `homeTeam` as well,
168+
and the reverse-direction pair (`homeGames`) likewise.
169+
170+
Flattened (many-to-many) ObjRelationships traverse more than one DbRelationship
171+
(`db-relationship-path="artistGroupArray.toArtist"`), so there is no 1:1 name to mirror — just keep
172+
each individual DbRelationship name sane on its own and fix any that are numbered collisions.
173+
174+
See `model-naming-rename-safety.md` for exactly what to update when you rename any of these.
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
<!--
2+
Licensed to the Apache Software Foundation (ASF) under one
3+
or more contributor license agreements. See the NOTICE file
4+
distributed with this work for additional information
5+
regarding copyright ownership. The ASF licenses this file
6+
to you under the Apache License, Version 2.0 (the
7+
"License"); you may not use this file except in compliance
8+
with the License. You may obtain a copy of the License at
9+
10+
https://www.apache.org/licenses/LICENSE-2.0
11+
12+
Unless required by applicable law or agreed to in writing,
13+
software distributed under the License is distributed on an
14+
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
KIND, either express or implied. See the License for the
16+
specific language governing permissions and limitations
17+
under the License.
18+
-->
19+
# Rename safety — cross-reference checklist
20+
21+
Reference for the `cayenne-model-naming` skill. A model name is referenced from several other places
22+
in the same DataMap (and from user Java code). Renaming an element without updating its references
23+
leaves a dangling reference that fails Cayenne validation at load time, or silently changes behavior.
24+
For every rename, walk the checklist for that element type. Element shapes are in `datamap-schema.md`.
25+
26+
## Golden rules
27+
28+
- **Rename the element, don't repoint it.** You change a `name`; you never touch the `db-attribute-path`
29+
or `db-relationship-path` *targets* to point somewhere else. Those paths only change when the thing
30+
they point *to* was itself renamed (DbAttribute/DbRelationship), and then only the matching segment.
31+
- **Names are unique within their scope.** Within an ObjEntity, attributes and relationships share one
32+
namespace; within a DbEntity, likewise; ObjEntity names are unique within the DataMap; DbRelationship
33+
names are unique within their source DbEntity. Never introduce a collision — collisions are exactly
34+
what produced the numbered names (`team1`) you're removing.
35+
- **Batch, then re-validate.** Apply all renames, then re-walk every checklist below to confirm no
36+
reference dangles.
37+
- **Name before you generate.** Renames must happen *before* `cayenne-cgen`, so classes are generated
38+
with the final names. Renaming after generation orphans the old `.java` files.
39+
40+
## Rename an ObjEntity `name` (`X``Y`)
41+
42+
| Update | Where |
43+
|---|---|
44+
| `className` simple name | Same `<obj-entity>`. Keep it equal to the new `name` → the generated class is `Y`. Old `X.java` / `_X.java` become orphaned; regenerate and delete the stale pair. |
45+
| `source="X"` / `target="X"` | Every `<obj-relationship>` in the DataMap — on **any** entity, not just this one. |
46+
| `root-name="X"` | Every `<query>` with `root="obj-entity"`. |
47+
| entity name in EJBQL | `<ejbql>` bodies (`select a from X a``... from Y a`). |
48+
| `result-entity="X"` | `<query type="ProcedureQuery">`. |
49+
| Java | `ObjectSelect.query(X.class)` tracks `className`, so updating `className` + regenerating covers it. Also fix string-based entity lookups (`context.newObject("X")`, `objectSelect("X")`) and EJBQL strings in code. |
50+
51+
The `dbEntityName` does **not** change — the DbEntity keeps its DB-derived name.
52+
53+
## Rename an ObjAttribute `name`
54+
55+
| Update | Where |
56+
|---|---|
57+
| query qualifiers / orderings | `<qualifier>` and `<ordering>` bodies that reference the old property name. |
58+
| EJBQL | `<ejbql>` paths (`a.oldName``a.newName`). |
59+
| Java | Generated getter/setter changes; fix `Expression`/`Property` paths and `ObjectSelect` column refs in user code. |
60+
61+
`db-attribute-path` is **unaffected** — it names the DB column, which didn't change. Uniqueness is
62+
within the owning ObjEntity (attrs + rels).
63+
64+
## Rename an ObjRelationship `name`
65+
66+
| Update | Where |
67+
|---|---|
68+
| prefetches | `<prefetch>` bodies and any dotted path that traverses this relationship. |
69+
| qualifiers / orderings / EJBQL | Expression paths that step through the old relationship name. |
70+
| Java | Generated getter/setter changes; fix prefetch/expression paths in user code. |
71+
72+
`db-relationship-path` is **unaffected** — it names DbRelationships, not this ObjRelationship's own
73+
name. Uniqueness is within the owning ObjEntity.
74+
75+
## Rename a DbRelationship `name` (`a``b`)
76+
77+
| Update | Where |
78+
|---|---|
79+
| `db-relationship-path` **segments** | Every `<obj-relationship>` whose `db-relationship-path` contains `a`**including inside dotted flattened chains**. E.g. path `artistGroupArray.toArtist`, renaming `toArtist``b` gives `artistGroupArray.b`. These references can live on entities other than the DbRelationship's source. |
80+
81+
No Java impact — DbRelationships are a DB-layer concept. Uniqueness is within the source DbEntity
82+
(attrs + rels). Keep the name mirrored with its paired ObjRelationship per direction (see
83+
`model-naming-conventions.md`).
84+
85+
## Paired renames
86+
87+
A single FK is usually four coordinated names: two DbRelationships (one per direction) and two
88+
ObjRelationships built on them. When you fix a numbered-collision pair, rename all four so both ends
89+
read as opposite roles and each ObjRelationship's name still mirrors its DbRelationship:
90+
91+
```
92+
DbRelationship ARTIST_ID (Game→Team, to-one) homeTeam
93+
DbRelationship ARTIST_ID (Team→Game, to-many) homeGames
94+
ObjRelationship (Game→Team) homeTeam (mirrors db-rel homeTeam)
95+
ObjRelationship (Team→Game) homeGames (mirrors db-rel homeGames)
96+
```
97+
98+
After renaming a DbRelationship, remember its name appears in the *other* direction's ObjRelationship
99+
`db-relationship-path` only when that path traverses it (flattened case) — a simple one-hop
100+
ObjRelationship's `db-relationship-path` is just its own backing DbRelationship's new name.

ai-plugin/skills/cayenne-db-import/SKILL.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ Once the import succeeds:
118118

119119
- Tell the user to **save the project** in the Modeler if it is open (File → Save). The dialog settings persist as a `<dbImport>` block inside the DataMap for repeat runs.
120120
- Hand off to `cayenne-cgen` to regenerate Java classes for the new/changed entities. Quote the DataMap name so the cgen skill can pass it to `cgen_run`.
121+
- If the imported names look awkward (run-together words, `team1`-style relationship names, a table prefix leaking into relationships), you can **offer** the `cayenne-model-naming` skill to polish them — ideally before cgen. Only run it if the user asks; do not invoke it automatically.
121122
- If the DB has columns that don't follow the user's preferred naming, recommend tweaking the naming strategy and re-running.
122123

123124
## Anti-patterns

0 commit comments

Comments
 (0)