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 2b0e3e1afefd2e97b796f4ccef989c0c727b5ed4 Author: Andrus Adamchik <[email protected]> AuthorDate: Thu May 21 15:45:09 2026 -0400 CAY-2946 Claude Code "plugin" for agentic coding with Cayenne --- RELEASE-NOTES.txt | 1 + ai-plugin/.claude-plugin/plugin.json | 6 + ai-plugin/README.md | 61 ++++++ ai-plugin/references/cgen-config.md | 116 ++++++++++ ai-plugin/references/datamap-schema.md | 233 +++++++++++++++++++++ ai-plugin/references/dbimport-config.md | 93 ++++++++ ai-plugin/references/mcp-tools.md | 81 +++++++ ai-plugin/references/project-descriptor-schema.md | 83 ++++++++ ai-plugin/references/project-layout.md | 52 +++++ ai-plugin/references/query-api.md | 224 ++++++++++++++++++++ ai-plugin/references/runtime-api.md | 143 +++++++++++++ ai-plugin/skills/cayenne-cgen/SKILL.md | 77 +++++++ ai-plugin/skills/cayenne-modeler/SKILL.md | 61 ++++++ ai-plugin/skills/cayenne-modeling/SKILL.md | 74 +++++++ ai-plugin/skills/cayenne-query/SKILL.md | 105 ++++++++++ ai-plugin/skills/cayenne-reverse-engineer/SKILL.md | 80 +++++++ ai-plugin/skills/cayenne-runtime/SKILL.md | 86 ++++++++ 17 files changed, 1576 insertions(+) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index c5ad64fdc..5f298bc8b 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -43,6 +43,7 @@ CAY-2940 Upgrade to SLF4J version 2.0.17 CAY-2942 CayenneModeler MCP: cgen_run tool CAY-2943 CayenneModeler MCP: open_project tool CAY-2945 CayenneModeler universal "main" +CAY-2946 Claude Code "plugin" for agentic coding with Cayenne Bug Fixes: diff --git a/ai-plugin/.claude-plugin/plugin.json b/ai-plugin/.claude-plugin/plugin.json new file mode 100644 index 000000000..94108acea --- /dev/null +++ b/ai-plugin/.claude-plugin/plugin.json @@ -0,0 +1,6 @@ +{ + "name": "apache-cayenne", + "description": "Apache Cayenne ORM — design DataMaps, reverse-engineer schemas, generate classes, and write runtime code. Drives the cayenne MCP server when present.", + "version": "5.0.0", + "author": { "name": "Apache Cayenne" } +} diff --git a/ai-plugin/README.md b/ai-plugin/README.md new file mode 100644 index 000000000..9305ba23b --- /dev/null +++ b/ai-plugin/README.md @@ -0,0 +1,61 @@ +# apache-cayenne — Claude Code plugin + +Apache Cayenne ORM workflows for Claude Code. + +This plugin teaches Claude how to: + +- Edit Cayenne DataMap (`*.map.xml`) and project descriptor (`cayenne-*.xml`) files a-la-carte (add entities, relationships, queries, embeddables). +- Reverse-engineer a database schema into a DataMap by driving CayenneModeler. +- Regenerate Java entity classes from a DataMap. +- Bootstrap `CayenneRuntime` in a Java application and write `ObjectSelect` / `SQLSelect` queries. + +The plugin assumes downstream **users of Cayenne** writing their own Java apps. It does *not* cover contributor workflows for hacking on Cayenne itself. + +## First-time setup + +> **Cayenne 5.0+ only.** The MCP server ships with Cayenne 5.0. Skills that depend on it (`cayenne-cgen`, `cayenne-modeler`, `cayenne-reverse-engineer`) will not work against earlier Cayenne versions — there is no MCP server to register. The XML-editing skills (`cayenne-modeling`) and runtime/query skills target Cayenne 5.0 idioms too; for older Cayenne, this plugin is not the right tool. + +1. **Register the Cayenne MCP server.** Class generation (`cgen_run`) and opening the Modeler GUI (`open_project`) both go through this server. +TODO: Full instructions will be on the website shorly. + +```bash +claude mcp add cayenne --scope user -- java -jar /path/to/cayenne-mcp-server-<VERSION>.jar +``` + +2. **Verify** the server is registered with `claude mcp list` — you should see an entry named `cayenne` (the MCP server alias from the command above, separate from this plugin's `apache-cayenne` name) showing as connected. + +That's it. The skills detect the MCP server at runtime; if it isn't connected they point back at this README instead of falling back to Maven or Gradle goals. + +## What's in here + +``` +ai-plugin/ +├── .claude-plugin/plugin.json # manifest +├── README.md # this file +├── skills/ # auto-triggering workflows +│ ├── cayenne-modeling/ # edit *.map.xml and cayenne-*.xml +│ ├── cayenne-reverse-engineer/# import a DB schema (Modeler GUI) +│ ├── cayenne-cgen/ # regenerate Java classes via MCP +│ ├── cayenne-modeler/ # open the GUI on a project +│ ├── cayenne-runtime/ # bootstrap CayenneRuntime in an app +│ └── cayenne-query/ # write ObjectSelect / SQLSelect queries +└── references/ # source-of-truth docs loaded by skills + ├── project-layout.md + ├── datamap-schema.md + ├── project-descriptor-schema.md + ├── dbimport-config.md + ├── cgen-config.md + ├── runtime-api.md + ├── query-api.md + └── mcp-tools.md +``` + +Each skill is a thin trigger that loads the right reference docs and walks Claude through the workflow. + +ORM workflows go through: + +1. Direct XML edits — primary path for a-la-carte model changes. +2. MCP `cgen_run` — class generation. +3. MCP `open_project` → CayenneModeler GUI — full DB sync and visual editing. + +and will NOT use Maven or Gradle plugins diff --git a/ai-plugin/references/cgen-config.md b/ai-plugin/references/cgen-config.md new file mode 100644 index 000000000..f085e9de0 --- /dev/null +++ b/ai-plugin/references/cgen-config.md @@ -0,0 +1,116 @@ +# Class generation config (`<cgen>`) + +Reference for the embedded `<cgen>` block inside a DataMap that drives `mcp__cayenne__cgen_run`. + +## XML shape (embedded in a DataMap) + +```xml +<data-map xmlns="http://cayenne.apache.org/schema/12/modelMap" ...> + ...entities, relationships, etc... + + <cgen xmlns="http://cayenne.apache.org/schema/12/cgen"> + <destDir>../java</destDir> + <mode>entity</mode> + <excludeEntities></excludeEntities> + <excludeEmbeddables></excludeEmbeddables> + <outputPattern>*.java</outputPattern> + <makePairs>true</makePairs> + <usePkgPath>true</usePkgPath> + <overwrite>false</overwrite> + <createPropertyNames>false</createPropertyNames> + <createPKProperties>true</createPKProperties> + </cgen> +</data-map> +``` + +Note the **different namespace** for `<cgen>` (`http://cayenne.apache.org/schema/12/cgen`) — it is a separate schema embedded in the modelMap schema. + +## Field reference + +### Required + +| Field | Default | Meaning | +|---|---|---| +| `<destDir>` | — | Output directory, **relative to the project XML directory** (the directory containing `cayenne-*.xml`). Typical values: `.`, `../java`, `../../main/java`. Absolute paths also work. | +| `<mode>` | `entity` | What to generate. `entity` = per-entity classes. `all` = per-entity classes + a single DataMap class with named-query helpers. | + +### Pairs vs single class + +| Field | Default | Meaning | +|---|---|---| +| `<makePairs>` | `true` | When `true`, generate two files per entity: `_Artist.java` (regenerated, never edit) and `Artist.java` (user subclass, edit freely). When `false`, generate a single `Artist.java` — overwritten each run. **Almost always leave at `true`.** | +| `<overwrite>` | `false` | Only meaningful when `makePairs=false`. When `true`, the single subclass is overwritten on each run. Dangerous if user code lives there. | + +### Filtering + +| Field | Default | Meaning | +|---|---|---| +| `<excludeEntities>` | (empty) | Comma-separated ObjEntity names to skip. Inverse — what's *included* is everything else. | +| `<excludeEmbeddables>` | (empty) | Comma-separated embeddable class names to skip. | + +### Output + +| Field | Default | Meaning | +|---|---|---| +| `<outputPattern>` | `*.java` | File name pattern. `*` is replaced by the class name. Change to `*.kt` for Kotlin templates. | +| `<encoding>` | platform default | Character encoding for generated files. | +| `<usePkgPath>` | `true` | When `true`, generated files go into subdirectories matching their Java package (`com/example/Artist.java`). When `false`, all files are flat in `destDir`. | +| `<superPkg>` | (empty) | Override package for generated superclasses (`_Artist`). By default, `<defaultPackage>.auto`. | + +### Templates + +| Field | Default | Meaning | +|---|---|---| +| `<template>` | built-in `subclass.vm` | Velocity template for the user subclass (`Artist.java`). Use a CDATA path to a custom `.vm` file. | +| `<superTemplate>` | built-in `superclass.vm` | Template for the auto-generated superclass (`_Artist.java`). | +| `<embeddableTemplate>` | built-in | Template for embeddable subclass. | +| `<embeddableSuperTemplate>` | built-in | Template for embeddable superclass. | +| `<dataMapTemplate>` | built-in | Template for the DataMap helper class (only used when `<mode>` is `all`). | +| `<dataMapSuperTemplate>` | built-in | Superclass template for the DataMap helper. | + +Custom templates are rare. Skip unless the user explicitly asks for them. + +### Properties + +| Field | Default | Meaning | +|---|---|---| +| `<createPropertyNames>` | `false` | When `true`, also generate `public static final String` constants for each attribute name. Legacy — most code uses the typed `Property<>` constants instead. | +| `<createPKProperties>` | `true` | When `true`, generate `Property<>` constants for PK columns too. | +| `<externalToolConfig>` | (empty) | Free-form config consumed by external tools (e.g., Kotlin generator extensions). | + +## Modeler equivalent + +In CayenneModeler: select the DataMap → "Class Generation" tab. Every field above has a UI control. Saving the project persists them back as `<cgen>` in the DataMap. + +## How `mcp__cayenne__cgen_run` uses this + +The MCP tool: + +1. Loads the project at `projectPath`. +2. Finds the named DataMap (`dataMap` argument). +3. Reads its embedded `<cgen>` block. +4. Resolves `destDir` relative to the project file. +5. Runs the generator. Returns a JSON object listing files `written`, files `skipped` (already up-to-date), and any `errors`. + +If the DataMap has no `<cgen>` block, the tool returns an error and the user must add one — typically via the `cayenne-modeling` skill or the Modeler GUI. + +## Recommended starting config + + +```xml +<cgen xmlns="http://cayenne.apache.org/schema/12/cgen"> + <destDir>../java</destDir> + <mode>entity</mode> + <makePairs>true</makePairs> + <usePkgPath>true</usePkgPath> + <createPKProperties>true</createPKProperties> +</cgen> +``` + +Assuming the project XML lives at `src/main/resources/cayenne-mydb.xml`, this writes to `src/main/java/<package>/`. + +## Anti-patterns + +- Setting `<makePairs>false</makePairs>` to "simplify" — you lose the ability to add user code without it being overwritten on the next cgen run. +- Editing `_<Entity>.java` files (the superclasses). They are regenerated. Customize `<Entity>.java` (the subclass) instead. +- `<destDir>` as an absolute path checked into source — breaks reproducibility for other developers. Use a relative path. diff --git a/ai-plugin/references/datamap-schema.md b/ai-plugin/references/datamap-schema.md new file mode 100644 index 000000000..3c670d2c1 --- /dev/null +++ b/ai-plugin/references/datamap-schema.md @@ -0,0 +1,233 @@ +# DataMap XML schema (`*.map.xml`) + +Reference for editing Cayenne DataMap files directly. + +- **Namespace**: `http://cayenne.apache.org/schema/12/modelMap` +- **XSD**: `http://cayenne.apache.org/schema/12/modelMap.xsd` +- **Project version**: `12` (Cayenne 5.0) +- **Working example**: `cayenne-ant/src/test/resources/testmap.map.xml` + +## Root element + +```xml +<?xml version="1.0" encoding="utf-8"?> +<data-map xmlns="http://cayenne.apache.org/schema/12/modelMap" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://cayenne.apache.org/schema/12/modelMap http://cayenne.apache.org/schema/12/modelMap.xsd" + project-version="12"> + ... +</data-map> +``` + +## DataMap properties + +Top-level `<property>` children of `<data-map>` configure DataMap-wide settings. The two used in practice: + +```xml +<property name="defaultPackage" value="com.example.model"/> +<property name="defaultSuperclass" value="org.apache.cayenne.GenericPersistentObject"/> +``` + +- `defaultPackage` — Java package for generated classes when an `obj-entity` `className` is a short name. +- `defaultSuperclass` — superclass for generated `_<Name>` superclasses. Default is `BaseDataObject`. + +## Element order + +The schema enforces this order inside `<data-map>`: + +1. `<property>` * +2. `<procedure>` * +3. `<embeddable>` * +4. `<db-entity>` * +5. `<obj-entity>` * +6. `<db-relationship>` * +7. `<obj-relationship>` * +8. `<query>` * +9. `<cgen>` ? (different namespace, embedded) +10. `<dbImport>` ? (different namespace, embedded) + +When inserting elements by hand, respect this order or Cayenne's parser will reject the file. + +## `<db-entity>` — database table + +```xml +<db-entity name="ARTIST" catalog="public" schema="public"> + <db-attribute name="ARTIST_ID" type="BIGINT" isPrimaryKey="true" isMandatory="true"/> + <db-attribute name="ARTIST_NAME" type="VARCHAR" length="254" isMandatory="true"/> + <db-attribute name="DATE_OF_BIRTH" type="DATE"/> +</db-entity> +``` + +Attribute fields: + +| Attribute | Meaning | +|---|---| +| `name` | Column name (case-preserved as-is from the DB) | +| `type` | JDBC type name: `INTEGER`, `BIGINT`, `VARCHAR`, `CHAR`, `DATE`, `TIMESTAMP`, `BOOLEAN`, `BIT`, `NUMERIC`, `DECIMAL`, `FLOAT`, `DOUBLE`, `BLOB`, `CLOB`, `VARBINARY`, etc. | +| `length` | Column length (for VARCHAR, CHAR, VARBINARY, NUMERIC) | +| `scale` | Decimal scale (NUMERIC, DECIMAL) | +| `isPrimaryKey` | `true` for PK columns | +| `isMandatory` | `true` for NOT NULL | +| `isGenerated` | `true` for DB-generated columns (identity, sequence) | + +## `<obj-entity>` — Java object mapped to a DbEntity + +```xml +<obj-entity name="Artist" className="com.example.Artist" dbEntityName="ARTIST"> + <obj-attribute name="artistName" type="java.lang.String" db-attribute-path="ARTIST_NAME"/> + <obj-attribute name="dateOfBirth" type="java.util.Date" db-attribute-path="DATE_OF_BIRTH"/> +</obj-entity> +``` + +Element attributes: + +| Attribute | Meaning | +|---|---| +| `name` | Object name (used in queries) | +| `className` | Fully-qualified Java class. If using DataMap `defaultPackage`, a short name works. | +| `superClassName` | Optional explicit superclass | +| `dbEntityName` | Matching `<db-entity name="...">` | +| `superEntityName` | For inheritance — name of parent ObjEntity | +| `readOnly` | `true` to disallow writes | +| `abstract` | `true` for abstract entities (single-table inheritance) | + +`obj-attribute`: + +| Attribute | Meaning | +|---|---| +| `name` | Java property name | +| `type` | Java type, FQN — `java.lang.String`, `java.lang.Integer`, `java.util.Date`, `java.math.BigDecimal`, `byte[]`, `boolean`, etc. | +| `db-attribute-path` | Column name. May be a dotted path through a `db-relationship` for derived attributes: `toArtist.ARTIST_NAME`. | + +PK columns are not normally mapped as `obj-attribute` — they're handled implicitly. Map a PK column only if `meaningfulPK` (you want to expose the PK value to the Java side). + +## `<db-relationship>` — FK join in the DB layer + +```xml +<db-relationship name="paintingArray" source="ARTIST" target="PAINTING" toMany="true"> + <db-attribute-pair source="ARTIST_ID" target="ARTIST_ID"/> +</db-relationship> +``` + +| Attribute | Meaning | +|---|---| +| `name` | Identifier; referenced by `obj-relationship`'s `db-relationship-path` | +| `source` | Owning DbEntity name | +| `target` | Target DbEntity name | +| `toMany` | `true` for one-to-many or many-to-many side | +| `toDependentPK` | `true` when the target row's PK depends on this FK (typical for one-to-one or master/detail) | + +Each `<db-attribute-pair>` is one column of the join. Compound joins use multiple `<db-attribute-pair>` elements. + +**Symmetry.** Most FKs need *two* `<db-relationship>` entries, one from each side. They are not auto-derived. + +## `<obj-relationship>` — object-layer view of a db-relationship + +```xml +<obj-relationship name="paintings" + source="Artist" + target="Painting" + deleteRule="Cascade" + db-relationship-path="paintingArray"/> +``` + +| Attribute | Meaning | +|---|---| +| `name` | Java property name (typically plural for to-many) | +| `source` | Owning ObjEntity name | +| `target` | Target ObjEntity name | +| `deleteRule` | `Nullify`, `Cascade`, `Deny`, or `NoAction` | +| `db-relationship-path` | One or more `<db-relationship>` names, dot-separated for flattened (many-to-many) relationships: `artistGroupArray.toGroup` | + +Every `obj-relationship` requires a matching `db-relationship` (or chain) — never add one without the backing DB-layer relationship. + +## `<embeddable>` — value object without identity + +```xml +<embeddable className="com.example.Address"> + <embeddable-attribute name="street" type="java.lang.String" db-attribute-name="STREET"/> + <embeddable-attribute name="city" type="java.lang.String" db-attribute-name="CITY"/> +</embeddable> +``` + +To embed it inside an ObjEntity, use `<embedded-attribute>`: + +```xml +<obj-entity name="User" className="com.example.User" dbEntityName="USER"> + <embedded-attribute name="homeAddress" type="com.example.Address"/> + <embedded-attribute name="workAddress" type="com.example.Address"> + <embeddable-attribute-override name="street" db-attribute-path="WORK_STREET"/> + <embeddable-attribute-override name="city" db-attribute-path="WORK_CITY"/> + </embedded-attribute> +</obj-entity> +``` + +`<embeddable-attribute-override>` is only needed when the host columns differ from the embeddable's default `db-attribute-name`. + +## `<procedure>` — stored procedure + +```xml +<procedure name="search_artists"> + <procedure-parameter name="name_filter" type="VARCHAR" length="254" direction="in"/> + <procedure-parameter name="result_count" type="INTEGER" direction="out"/> +</procedure> +``` + +`direction` is `in`, `out`, or `in_out`. + +## `<query>` — named query + +Three flavors, all named with `<query name="...">` and selected by `type=`: + +### SelectQuery + +```xml +<query name="ArtistsByName" type="SelectQuery" root="obj-entity" root-name="Artist"> + <property name="cayenne.GenericSelectQuery.cacheStrategy" value="LOCAL_CACHE"/> + <qualifier><![CDATA[artistName like $name]]></qualifier> + <ordering descending="true"><![CDATA[dateOfBirth]]></ordering> + <prefetch>paintings</prefetch> +</query> +``` + +### SQLTemplate + +```xml +<query name="LowercasedArtists" type="SQLTemplate" root="data-map" root-name="testmap"> + <property name="cayenne.SQLTemplate.columnNameCapitalization" value="LOWER"/> + <sql><![CDATA[select * from ARTIST]]></sql> + <sql adapter-class="org.apache.cayenne.dba.postgres.PostgresAdapter"><![CDATA[select * from artist]]></sql> +</query> +``` + +Use a second `<sql>` with `adapter-class=` to vary by DB adapter. SQLTemplate placeholders use Velocity syntax — `#bind($paramName)` for parameters. + +### EJBQLQuery + +```xml +<query name="ArtistByName" type="EJBQLQuery"> + <ejbql><![CDATA[select a from Artist a where a.artistName = ?1]]></ejbql> +</query> +``` + +### ProcedureQuery + +```xml +<query name="SearchArtists" type="ProcedureQuery" root="procedure" root-name="search_artists" result-entity="Artist"/> +``` + +## `<cgen>` — embedded code-gen config + +A separate namespace, embedded directly in the DataMap. See `cgen-config.md` for fields. + +## `<dbImport>` — embedded reverse-engineering config + +A separate namespace, used by the Modeler's reverse-engineering wizard to persist its options. See `dbimport-config.md` for fields. + +## Anti-patterns to avoid + +- Adding an `obj-relationship` without a backing `db-relationship` — Cayenne will validate-fail at runtime load. +- Setting `db-attribute` `type` without `length` for VARCHAR/CHAR +- Using a Java primitive (`int`, `long`) for an `obj-attribute` `type` when the column is nullable — primitives can't represent NULL; use the wrapper (`java.lang.Integer`). +- Reordering top-level elements — the schema requires the order listed above. +- Hand-editing `_<Entity>` superclass `.java` files — they are regenerated by cgen and will be overwritten. Edit the user subclass instead. diff --git a/ai-plugin/references/dbimport-config.md b/ai-plugin/references/dbimport-config.md new file mode 100644 index 000000000..5675ec487 --- /dev/null +++ b/ai-plugin/references/dbimport-config.md @@ -0,0 +1,93 @@ +# Reverse-engineering config (`<dbImport>`) + +Reference for the options shown by the CayenneModeler reverse-engineering wizard, persisted as a `<dbImport>` block inside a DataMap. + +## XML shape (persisted inside a DataMap) + +```xml +<dbImport xmlns="http://cayenne.apache.org/schema/12/dbimport"> + <catalog> + <name>public</name> + <schema> + <name>app</name> + <includeTable> + <pattern>CUSTOMER.*</pattern> + </includeTable> + <excludeTable> + <pattern>TMP_.*</pattern> + </excludeTable> + </schema> + </catalog> + <tableTypes> + <tableType>TABLE</tableType> + <tableType>VIEW</tableType> + </tableTypes> + <defaultPackage>com.example.model</defaultPackage> + <forceDataMapCatalog>false</forceDataMapCatalog> + <forceDataMapSchema>false</forceDataMapSchema> + <meaningfulPkTables></meaningfulPkTables> + <namingStrategy>org.apache.cayenne.dbsync.naming.DefaultObjectNameGenerator</namingStrategy> + <skipPrimaryKeyLoading>false</skipPrimaryKeyLoading> + <skipRelationshipsLoading>false</skipRelationshipsLoading> + <stripFromTableNames></stripFromTableNames> + <useJava7Types>false</useJava7Types> +</dbImport> +``` + +## Field reference + +### Filtering + +| Field | Type | Meaning | +|---|---|---| +| `<catalog><name>` | string | DB catalog to import from. Many databases have a single default catalog. | +| `<schema><name>` | string | DB schema (Postgres `public`, MSSQL `dbo`, etc.). | +| `<includeTable><pattern>` | regex | Tables to include. Multiple `<includeTable>` elements union. Default: include all. | +| `<excludeTable><pattern>` | regex | Tables to skip. Multiple `<excludeTable>` elements union. | +| `<includeColumn><pattern>` | regex | Columns to include (rarely needed). | +| `<excludeColumn><pattern>` | regex | Columns to skip — useful for system columns (`CREATED_AT`, `ROWVERSION`). | +| `<includeProcedure><pattern>` | regex | Stored procedures to include. By default, none are imported. | +| `<excludeProcedure><pattern>` | regex | Stored procedures to skip. | +| `<tableTypes><tableType>` | enum | Which JDBC table types to import. Typical values: `TABLE`, `VIEW`, `SYSTEM TABLE`, `GLOBAL TEMPORARY`, `LOCAL TEMPORARY`, `ALIAS`, `SYNONYM`. If empty, defaults to `TABLE`. | + +### Generation + +| Field | Type | Default | Meaning | +|---|---|---|---| +| `<defaultPackage>` | string | (empty) | Java package for generated ObjEntity classes. If the DataMap already has `defaultPackage`, that wins. | +| `<namingStrategy>` | FQN string | `org.apache.cayenne.dbsync.naming.DefaultObjectNameGenerator` | Class implementing `ObjectNameGenerator` that converts `CUSTOMER_ORDER` → `CustomerOrder`, `customer_id` → `customerId`. Plug in a custom one for non-default conventions. | +| `<stripFromTableNames>` | regex | (empty) | Regex applied to each table name *before* naming. E.g. `^TBL_` strips a `TBL_` prefix so `TBL_CUSTOMER` → `Customer`. | +| `<meaningfulPkTables>` | comma-separated regex | (empty) | Tables whose PK columns should be mapped as ObjAttributes (visible on the Java side). `*` matches all. By default PKs are hidden. | +| `<useJava7Types>` | boolean | `false` | When `true`, generate `java.util.Date` instead of `java.time.LocalDate`/`LocalDateTime`. Use only when targeting legacy JVMs. | + +### Behavior toggles + +| Field | Type | Default | Meaning | +|---|---|---|---| +| `<skipPrimaryKeyLoading>` | boolean | `false` | When `true`, PK metadata is not read from the DB. Rare. | +| `<skipRelationshipsLoading>` | boolean | `false` | When `true`, FKs are not read. Useful when FKs are missing in the schema and modeled manually. | +| `<forceDataMapCatalog>` | boolean | `false` | When `true`, every imported DbEntity is tagged with the DataMap's catalog, overriding the DB's reported value. Use only when the DB catalog reported by JDBC is wrong or noisy. | +| `<forceDataMapSchema>` | boolean | `false` | Same as above for schema. | + +## How the Modeler wizard maps to these fields + +Walking the **Tools → Reengineer Database Schema** wizard, the screens correspond to: + +1. **Datasource** — JDBC connection (adapter, driver, URL, user/password). Not stored in `<dbImport>`; it's a one-shot connection. +2. **Configure** — filter tables/columns/procedures. Maps to `<includeTable>`, `<excludeTable>`, `<tableTypes>`, etc. +3. **Naming** — `<namingStrategy>`, `<stripFromTableNames>`, `<defaultPackage>`, `<meaningfulPkTables>`. +4. **Other options** — checkboxes for `skipPrimaryKeyLoading`, `skipRelationshipsLoading`, `forceDataMapCatalog`, `forceDataMapSchema`, `useJava7Types`. + +After running the wizard, the chosen settings are persisted as a `<dbImport>` block in the DataMap so subsequent re-imports re-use them. + +## Re-running an import + +A second import against the same DataMap is *merge-based*: existing entities/attributes/relationships that match the DB are preserved; new ones are added; the user is offered a chance to drop ones that no longer exist in the DB. + +User-edited names, types, and custom attributes generally survive re-import. Custom relationships that don't correspond to a DB FK do not survive. + +## Anti-patterns + +- Setting `forceDataMapCatalog` / `forceDataMapSchema` "just to be safe" — they suppress useful DB metadata and cause subtle bugs in multi-catalog setups. +- Using `useJava7Types=true` on a new project — `java.time` types are strictly better. Only enable for legacy. +- Importing without any `<includeTable>` filter on a large schema — you get every system table and view. diff --git a/ai-plugin/references/mcp-tools.md b/ai-plugin/references/mcp-tools.md new file mode 100644 index 000000000..6e9ec2f56 --- /dev/null +++ b/ai-plugin/references/mcp-tools.md @@ -0,0 +1,81 @@ +# Cayenne MCP tools + +The Cayenne MCP server (`cayenne-mcp-server` module) exposes Cayenne operations to AI agents over stdio. + +**Availability: Cayenne 5.0+ only.** The MCP server is a new component shipped alongside CayenneModeler starting with the 5.0 release. There is no MCP server for Cayenne 4.x or earlier — skills that depend on these tools (`cayenne-cgen`, `cayenne-modeler`, `cayenne-reverse-engineer`) cannot be used against pre-5.0 projects. + +Setup is documented in `cayenne-mcp-server/README.md` at the repo root. Quick form for Claude Code: + +```bash +claude mcp add cayenne --scope user -- java -jar /path/to/cayenne-mcp-server-<VERSION>.jar +``` + +Verify the server is registered with `claude mcp list` — you should see an entry named `cayenne` (the MCP server alias from the `claude mcp add cayenne` command above) showing as connected. This is unrelated to the plugin name `apache-cayenne`. + +## Tool: `cgen_run` + +Runs Cayenne's class generator for one DataMap, using the `<cgen>` config block embedded in that DataMap. + +**Arguments:** + +| Name | Type | Required | Description | +|---|---|---|---| +| `projectPath` | string | yes | Absolute path to the top-level Cayenne project descriptor (`cayenne-*.xml`), **not** a DataMap file. | +| `dataMap` | string | yes | Name of the target DataMap as it appears in the `<map name="...">` element of the project descriptor. Not a file path. | + +**Returns:** JSON object with fields: + +```json +{ + "status": "ok" | "error", + "summary": { "writtenCount": 3, "skippedCount": 12, "errorCount": 0 }, + "resolvedConfig": { "destDir": "...", "mode": "entity", ... }, + "writtenFiles": [{ "path": "...", "size": 1234 }], + "skippedFiles": [{ "path": "...", "reason": "up-to-date" }], + "errors": [] +} +``` + +Surface the `summary` to the user verbatim. List the first few `writtenFiles` paths; full list is informational. If `errors` is non-empty, those are blocking — show them. + +**Failure modes:** + +- DataMap has no `<cgen>` block → error; user must add one (see `cgen-config.md` for the XML to insert). +- `projectPath` not readable or not a valid Cayenne project → validation error. +- `dataMap` doesn't match any `<map name="...">` → validation error. + +**Source:** `cayenne-mcp-server/src/main/java/org/apache/cayenne/mcp/tools/cgen/CgenRunTool.java`. + +## Tool: `open_project` + +Launches CayenneModeler with a project file pre-loaded. + +**Arguments:** + +| Name | Type | Required | Description | +|---|---|---|---| +| `projectPath` | string | yes | Absolute path to the top-level Cayenne project descriptor (`cayenne-*.xml`). | + +**Behavior:** + +1. Locates the bundled CayenneModeler installation alongside the running MCP jar. +2. Spawns it with `--mcp-handshake <nonce>`. +3. Waits up to ~15 seconds for the Modeler to write a `java.util.prefs.Preferences` handshake confirming successful project load. +4. Returns once the handshake fires or times out. + +**Returns:** JSON object with `status` (`ok` / `error`), and on error a `code` indicating the failure (e.g., `modeler_not_found`, `project_not_found`, `handshake_timeout`). + +**When to call:** for reverse-engineering workflows (drives the user through the Modeler's import wizard), bulk visual editing, or when the user explicitly asks to open the Modeler. + +**Don't call it** as a fallback for simple XML edits — direct edits via the `cayenne-modeling` skill are faster and don't require the user to switch context. + +**Source:** `cayenne-mcp-server/src/main/java/org/apache/cayenne/mcp/tools/openproject/OpenProjectTool.java`. + +## Detecting whether the server is connected + +The MCP tools surface in this session under names `mcp__cayenne__cgen_run` and `mcp__cayenne__open_project`. If they are not in the available tools, the server is not registered. + +When unavailable: + +- Do **not** fall back to `mvn cayenne:cgen` or any Gradle equivalent — those build plugins are practically deprecated and are intentionally out of scope for this Claude Code plugin. +- Point the user at `cayenne-mcp-server/README.md` for setup and stop. diff --git a/ai-plugin/references/project-descriptor-schema.md b/ai-plugin/references/project-descriptor-schema.md new file mode 100644 index 000000000..b8833a8e3 --- /dev/null +++ b/ai-plugin/references/project-descriptor-schema.md @@ -0,0 +1,83 @@ +# Project descriptor XML schema (`cayenne-*.xml`) + +Reference for editing the top-level Cayenne project descriptor. + +- **Namespace**: `http://cayenne.apache.org/schema/12/domain` +- **XSD**: `http://cayenne.apache.org/schema/12/domain.xsd` +- **Project version**: `12` (Cayenne 5.0) + +## Minimal descriptor + +```xml +<?xml version="1.0" encoding="utf-8"?> +<domain xmlns="http://cayenne.apache.org/schema/12/domain" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://cayenne.apache.org/schema/12/domain https://cayenne.apache.org/schema/12/domain.xsd" + project-version="12"> + <map name="mydb"/> +</domain> +``` + +This references a sibling `mydb.map.xml`. + +## With a DataNode (DB connection) + +Most production setups configure the DataSource in code via `CayenneRuntimeBuilder` (see `runtime-api.md`) rather than the descriptor — keeps secrets out of source control. But the descriptor *can* hold a DataNode: + +```xml +<domain ...> + <map name="mydb"/> + <node name="mydb-node" + factory="org.apache.cayenne.configuration.runtime.XMLPoolingDataSourceFactory" + schema-update-strategy="org.apache.cayenne.access.dbsync.SkipSchemaUpdateStrategy"> + <map-ref name="mydb"/> + <data-source> + <driver value="org.postgresql.Driver"/> + <url value="jdbc:postgresql://localhost:5432/mydb"/> + <connectionPool min="1" max="10"/> + <login userName="cayenne" password="secret"/> + </data-source> + </node> +</domain> +``` + +| Element/attribute | Meaning | +|---|---| +| `<map name="...">` | References a sibling DataMap file. Must match the `*.map.xml` file name minus the extension. | +| `<node name="...">` | A DataNode (database). The `factory=` selects the DataSource factory. | +| `factory=` | `XMLPoolingDataSourceFactory` reads the inline `<data-source>` block. `DBCPDataSourceFactory`, `JNDIDataSourceFactory` are alternatives. | +| `schema-update-strategy=` | What to do on startup when DB schema disagrees with the model. `SkipSchemaUpdateStrategy` is the safe default. | +| `<map-ref name="...">` | Which DataMap this node serves. A node can serve multiple maps. | + +## Multi-map projects + +A descriptor can reference multiple DataMaps: + +```xml +<domain ...> + <map name="customers"/> + <map name="orders"/> + <node name="primary"> + <map-ref name="customers"/> + <map-ref name="orders"/> + ... + </node> +</domain> +``` + +All referenced `*.map.xml` files must sit in the same directory as the descriptor. + +## Recommended pattern + +For a typical app: + +- Descriptor lists DataMaps with `<map>` only; **no** `<node>` element. +- Wire the DataSource in code via `CayenneRuntimeBuilder.dataSource(...)` or `.url(...).jdbcDriver(...)`. See `runtime-api.md`. + +This keeps DB credentials out of XML and lets the app pick the DataSource per environment (test, dev, prod). + +## Anti-patterns + +- Putting JDBC credentials in `cayenne-*.xml` and committing them to source control. Use code-level configuration. +- Referencing a DataMap that doesn't exist as a sibling file — the runtime will fail to load. +- Forgetting `project-version="12"` — older versions are silently rejected by Cayenne 5.0. diff --git a/ai-plugin/references/project-layout.md b/ai-plugin/references/project-layout.md new file mode 100644 index 000000000..4fba3528a --- /dev/null +++ b/ai-plugin/references/project-layout.md @@ -0,0 +1,52 @@ +# Cayenne project layout + +How a Cayenne project lays out on disk and how to locate the relevant files in a user's repo. + +## Two file types + +A Cayenne project consists of two kinds of XML files: + +1. **Project descriptor** — one per project. File name pattern: `cayenne-*.xml` (e.g. `cayenne-mydb.xml`). Root element `<domain>` in namespace `http://cayenne.apache.org/schema/12/domain`. Lists DataMaps and DataNodes. +2. **DataMap files** — one or more per project. File name pattern: `*.map.xml` (commonly `<name>.map.xml`, where `<name>` matches the `<map name="...">` reference in the project descriptor). Root element `<data-map>` in namespace `http://cayenne.apache.org/schema/12/modelMap`. Contains entities, relationships, queries. + +The descriptor and its DataMaps live in the **same directory**. The descriptor references DataMaps by name only (no path), so they must be siblings on the classpath. + +## Where to look + +Common locations, in order of likelihood: + +1. **`src/main/resources/`** of the module that bootstraps `CayenneRuntime`. This is the typical placement for a runtime-bundled project. Scan for `cayenne-*.xml` here first. +2. **`src/main/resources/<package>/`** — a subdirectory if the user wants to keep mapping files namespaced. +3. **Repo root or `cayenne/` subdir** — small projects sometimes keep mapping at the top. + +To find them programmatically: + +```bash +find . -name "cayenne-*.xml" -not -path "*/target/*" -not -path "*/build/*" +find . -name "*.map.xml" -not -path "*/target/*" -not -path "*/build/*" +``` + +Filter out `target/`, `build/`, and any test-resource paths unless the user is explicitly editing test fixtures. + +## Identifying the active project + +If multiple `cayenne-*.xml` files exist, identify the one the application actually uses: + +- Search the Java code for `CayenneRuntime.builder().addConfig("...")` — the string argument is the descriptor file name (e.g. `"cayenne-mydb.xml"`). +- Or look for `addConfigs("...")` for multi-config setups. +- If none is found, the runtime may rely on auto-loading (`cayenne-project.xml` is *not* a Cayenne convention — Cayenne does not auto-load by a default name). + +When ambiguous, ask the user which project they mean. Cache the answer for the rest of the session. + +## What goes where + +| Change | File to edit | +|---|---| +| Add/remove an `ObjEntity`, `DbEntity`, attribute, relationship, embeddable, named query | DataMap (`*.map.xml`) | +| Change a DataMap's `defaultPackage` or `defaultSuperclass` | DataMap (`*.map.xml`) — top-level `<property>` | +| Add/remove a DataMap from the project | Project descriptor (`cayenne-*.xml`) — `<map>` element | +| Add/configure a DataNode (DB connection) | Project descriptor (`cayenne-*.xml`) — `<node>` element | +| Embedded code-generation config (`<cgen>`) | DataMap (`*.map.xml`) | +| Embedded reverse-engineering config (`<dbImport>`) | DataMap (`*.map.xml`) | + +Schema details live in `datamap-schema.md` and `project-descriptor-schema.md`. diff --git a/ai-plugin/references/query-api.md b/ai-plugin/references/query-api.md new file mode 100644 index 000000000..1808c73cd --- /dev/null +++ b/ai-plugin/references/query-api.md @@ -0,0 +1,224 @@ +# Query API reference + +How to fetch and filter persistent objects with Cayenne 5.0. + +## Three query mechanisms + +| API | When to use | +|---|---| +| `ObjectSelect` | Default for selecting Java entities. Fluent, type-safe. | +| `SQLSelect` / `SQLExec` | Raw SQL with Cayenne parameter binding. | +| Named queries (XML) | When the same query is reused many places; lives in the DataMap. | + +## ObjectSelect — primary fluent query API + +`org.apache.cayenne.query.ObjectSelect`. Factory methods: + +```java +import org.apache.cayenne.query.ObjectSelect; + +// Fetch all +List<Artist> all = ObjectSelect.query(Artist.class).select(ctx); + +// Single by criteria +Artist picasso = ObjectSelect.query(Artist.class) + .where(Artist.ARTIST_NAME.eq("Picasso")) + .selectOne(ctx); + +// First matching, null if none +Artist maybe = ObjectSelect.query(Artist.class) + .where(Artist.ARTIST_NAME.eq("Picasso")) + .selectFirst(ctx); + +// Iterate large result without loading all +try (ResultIterator<Artist> it = ObjectSelect.query(Artist.class).iterator(ctx)) { + while (it.hasNextRow()) { + Artist a = it.nextRow(); + ... + } +} +``` + +### Filtering with Property constants + +cgen generates `Property<T>` constants on each entity superclass: `Artist.ARTIST_NAME`, `Artist.DATE_OF_BIRTH`, `Artist.PAINTINGS`. These are the recommended way to express filters because they are type-checked. + +```java +ObjectSelect.query(Artist.class) + .where(Artist.ARTIST_NAME.likeIgnoreCase("p%")) + .and(Artist.DATE_OF_BIRTH.between(d1, d2)) + .or(Artist.ARTIST_NAME.eq("Anonymous")) + .select(ctx); +``` + +Common predicate methods on `Property<T>`: + +- `.eq(value)`, `.ne(value)` +- `.lt(value)`, `.lte(value)`, `.gt(value)`, `.gte(value)` +- `.like(pattern)`, `.likeIgnoreCase(pattern)`, `.contains(s)`, `.startsWith(s)`, `.endsWith(s)` +- `.in(values...)`, `.nin(values...)` +- `.isNull()`, `.isNotNull()` +- `.between(min, max)` + +### Ordering + +```java +import org.apache.cayenne.query.Ordering; +import org.apache.cayenne.query.SortOrder; + +ObjectSelect.query(Artist.class) + .orderBy(Artist.ARTIST_NAME.asc()) + .orderBy(Artist.DATE_OF_BIRTH.desc()) + .select(ctx); +``` + +### Pagination + +```java +ObjectSelect.query(Artist.class) + .pageSize(50) // server-side pagination + .limit(1000) + .offset(0) + .select(ctx); +``` + +`pageSize` enables on-demand loading: the result list contains all PKs but only the requested page's full objects are materialized when accessed. + +### Prefetching (avoiding N+1) + +```java +// Joint: same query, single SQL +ObjectSelect.query(Artist.class) + .prefetch(Artist.PAINTINGS.joint()) + .select(ctx); + +// Disjoint: separate query per relationship +ObjectSelect.query(Artist.class) + .prefetch(Artist.PAINTINGS.disjoint()) + .select(ctx); + +// Disjoint-by-id: separate query keyed by source PKs +ObjectSelect.query(Artist.class) + .prefetch(Artist.PAINTINGS.disjointById()) + .select(ctx); +``` + +Rule of thumb: `joint()` for to-one, `disjoint()` for to-many that fans out modestly, `disjointById()` for large to-many. + +### Caching + +```java +ObjectSelect.query(Artist.class) + .localCache() // per-context cache + .select(ctx); + +ObjectSelect.query(Artist.class) + .sharedCache("artistGroup") // cross-context, named cache group + .select(ctx); +``` + +Use `sharedCache` for reference data. Invalidate by group via `cayenne-cache-invalidation` if installed. + +### Aggregates and column-only queries + +```java +import org.apache.cayenne.query.ColumnSelect; +import org.apache.cayenne.exp.property.PropertyFactory; + +// Single column +List<String> names = ObjectSelect.columnQuery(Artist.class, Artist.ARTIST_NAME).select(ctx); + +// Multiple columns +List<Object[]> rows = ObjectSelect.columnQuery(Artist.class, + Artist.ARTIST_NAME, Artist.DATE_OF_BIRTH).select(ctx); + +// Aggregate +long count = ObjectSelect.query(Artist.class).selectCount(ctx); +``` + +### Selecting by primary key + +```java +import org.apache.cayenne.query.SelectById; + +Artist a = SelectById.query(Artist.class, 42).selectOne(ctx); +``` + +## SQLSelect / SQLExec — raw SQL + +```java +import org.apache.cayenne.query.SQLSelect; +import org.apache.cayenne.query.SQLExec; + +// Select into entities +List<Artist> artists = SQLSelect.query(Artist.class, + "SELECT * FROM ARTIST WHERE ARTIST_NAME LIKE #bind($name)") + .params(Map.of("name", "P%")) + .select(ctx); + +// Select into raw rows +List<Object[]> rows = SQLSelect.scalarQuery(Object[].class, + "SELECT ARTIST_ID, ARTIST_NAME FROM ARTIST") + .select(ctx); + +// Execute (insert/update/delete) +int affected = SQLExec.query( + "UPDATE ARTIST SET ARTIST_NAME = UPPER(ARTIST_NAME)") + .update(ctx); +``` + +Parameter binding uses Velocity-like syntax: `#bind($name)`, `#bindEqual($name)`, `#bindNotEqual($name)`, `#chain('AND' 'condition' "$param")`. Never concatenate user input into SQL strings — use bindings. + +## Expression — programmatic predicates + +`Expression` is Cayenne's predicate AST. Most queries don't construct one directly — they let `Property` constants do it. + +### Preferred: build expressions from `Property` constants + +The `Property<T>` constants on each entity superclass (`Artist.ARTIST_NAME`, `Artist.DATE_OF_BIRTH`, `Artist.PAINTINGS`, etc., declared on the generated `_Artist` superclass and inherited by the user `Artist` subclass) return `Expression` objects from every predicate method. Compose them with `andExp` / `orExp`: + +```java +import org.apache.cayenne.exp.Expression; + +Expression e = Artist.ARTIST_NAME.eq("Picasso") + .andExp(Artist.DATE_OF_BIRTH.gt(someDate)); + +ObjectSelect.query(Artist.class).where(e).select(ctx); +``` + +This is the canonical pattern — see Cayenne's own test suites for many real-world examples (`Artist.ARTIST_NAME.eq(...)`, `Painting.TO_ARTIST.eq(artist)`, etc.). It's type-checked, refactor-safe, and survives column renames in the model. + +For relationship traversal, chain `Property` constants with `.dot(...)`: + +```java +Expression hasGuernica = Artist.PAINTINGS.dot(Painting.PAINTING_TITLE).eq("Guernica"); +``` + +### Fallback: `ExpressionFactory` (dynamic field names) + +Only when field selection is dynamic at runtime (e.g., the user picks a column from a UI dropdown): + +```java +import org.apache.cayenne.exp.ExpressionFactory; + +String runtimeField = pickField(); // not known at compile time +Expression e = ExpressionFactory.matchExp(runtimeField, value); +``` + +Avoid `ExpressionFactory.*` for static queries — `Property` constants are strictly better. + +### Fallback: `Expression.fromString(...)` (parsed string form) + +```java +Expression e = Expression.fromString("artistName = 'Picasso' and dateOfBirth > $cutoff"); +e = e.params(Map.of("cutoff", someDate)); +``` + +Supports `=`, `!=`, `<`, `>`, `<=`, `>=`, `like`, `likeIgnoreCase`, `in`, `between`, `and`, `or`, `not`, `null`. Dotted paths traverse relationships: `paintings.gallery.galleryName = 'MoMA'`. Useful when expressions are stored as strings (e.g. in config); otherwise prefer Property constants. + +## Anti-patterns + +- **String concatenation in raw SQL.** Always use `#bind($name)`. Concatenation is a SQL injection vector. +- **Using `selectOne` when multiple may match.** It throws. Use `selectFirst` if "any one" is okay. +- **Loading large result sets without `pageSize` or `iterator()`.** Both methods avoid materializing the full list. +- **N+1 from missing prefetch.** If you iterate `artists` and access `artist.getPaintings()` per artist, you'll fire one query per artist. Use `.prefetch(...)`. diff --git a/ai-plugin/references/runtime-api.md b/ai-plugin/references/runtime-api.md new file mode 100644 index 000000000..5ccc20b97 --- /dev/null +++ b/ai-plugin/references/runtime-api.md @@ -0,0 +1,143 @@ +# Runtime API reference + +How to bootstrap Cayenne in a Java application and use `ObjectContext` for CRUD. + +**Availability: Cayenne 5.0+ only.** `CayenneRuntime` and `CayenneRuntimeBuilder` are 5.0 names — they were called `ServerRuntime` and `ServerRuntimeBuilder` (in package `org.apache.cayenne.configuration.server`) in Cayenne 4.x. If the user's project is on 4.x, the patterns below won't compile — this plugin is targeted at 5.0. + +## CayenneRuntime — top-level container + +`org.apache.cayenne.runtime.CayenneRuntime` is the entry point. One per application lifetime (or per data domain in multi-DB setups). + +Two creation paths: + +### 1. With a built-in connection pool + +```java +import org.apache.cayenne.runtime.CayenneRuntime; +import org.apache.cayenne.runtime.CayenneRuntimeBuilder; + +CayenneRuntime runtime = CayenneRuntime.builder() + .addConfig("cayenne-mydb.xml") + .url("jdbc:postgresql://localhost:5432/mydb") + .jdbcDriver("org.postgresql.Driver") + .user("cayenne") + .password("secret") + .minConnections(1) + .maxConnections(10) + .build(); +``` + +### 2. With an external DataSource (Spring, HikariCP, Bootique, etc.) + +```java +import javax.sql.DataSource; + +DataSource ds = ...; // created elsewhere in your app + +CayenneRuntime runtime = CayenneRuntime.builder() + .addConfig("cayenne-mydb.xml") + .dataSource(ds) + .build(); +``` + +This path is preferred in production — your existing DI framework manages the pool, and Cayenne uses it. + +### CayenneRuntimeBuilder methods + +From `cayenne/src/main/java/org/apache/cayenne/runtime/CayenneRuntimeBuilder.java`: + +| Method | Purpose | +|---|---| +| `addConfig(String)` | Add a Cayenne project descriptor (`cayenne-*.xml`) location. Resolved via classpath. | +| `addConfigs(String...)` | Add multiple descriptors. | +| `dataSource(DataSource)` | Use an existing DataSource. Overrides anything in the XML. | +| `url(String)` | Built-in pool: JDBC URL. | +| `jdbcDriver(String)` | Built-in pool: driver class name. | +| `user(String)` / `password(String)` | Built-in pool: credentials. | +| `minConnections(int)` / `maxConnections(int)` | Built-in pool: bounds. | +| `validationQuery(String)` | Built-in pool: pool validation query (e.g. `SELECT 1`). | +| `maxQueueWaitTime(long)` | Built-in pool: max ms to wait for a connection. | +| `addModule(Module)` | Add a Cayenne DI module to override defaults. | +| `disableModulesAutoLoading()` | Skip the `META-INF/services/CayenneRuntimeModuleProvider` auto-load — for tightly-controlled DI. | +| `build()` | Construct the `CayenneRuntime`. | + +## Lifecycle + +`CayenneRuntime` is **expensive** to create — it parses XML, validates the model, sets up the connection pool. Create it **once**, share it for the application lifetime, and call `shutdown()` on app exit: + +```java +runtime.shutdown(); +``` + +Never create a new `CayenneRuntime` per request. + +## ObjectContext — the unit of work + +`org.apache.cayenne.ObjectContext` is the per-request handle for CRUD. Get one from the runtime: + +```java +ObjectContext ctx = runtime.newContext(); +``` + +Each `ObjectContext`: + +- Has its own session-level cache of loaded objects. +- Tracks modifications until you call `commitChanges()`. +- While ObjectContext is thread-safe, objects within in are **not thread-safe**. So read-only contexts can be used across threads, whereas read/write ones (those that change object state) would require a new context per request/thread. + +### Core methods + +```java +// Insert +Artist a = ctx.newObject(Artist.class); +a.setArtistName("Picasso"); +ctx.commitChanges(); + +// Read by ID +Artist a = SelectById.query(Artist.class, 42).selectOne(ctx); + +// Update +a.setArtistName("Pablo Picasso"); +ctx.commitChanges(); + +// Delete +ctx.deleteObject(a); +ctx.commitChanges(); + +// Rollback uncommitted changes +ctx.rollbackChanges(); +``` + +### Transactions + +`commitChanges()` runs inside an implicit transaction by default. For multi-step transactions, use `Cayenne.transactional`: + +```java +import org.apache.cayenne.tx.BaseTransaction; +import org.apache.cayenne.tx.TransactionalOperation; + +runtime.performInTransaction(() -> { + Artist a = ctx.newObject(Artist.class); + a.setArtistName("Picasso"); + ctx.commitChanges(); + + Painting p = ctx.newObject(Painting.class); + p.setArtist(a); + p.setTitle("Guernica"); + ctx.commitChanges(); + return null; +}); +``` + +If any step throws, the whole transaction rolls back. + +## DI: integrating with external containers + +Cayenne ships its own lightweight DI (`cayenne-di`). It does not require Spring, etc. To use Cayenne inside an application DI container (Spring, Bootique), bind `CayenneRuntime` as a singleton and decide how you want to create `ObjectContext` (request-scoped, method scoped, etc.) + +## Common mistakes + +- **Creating `CayenneRuntime` per request.** It's a heavyweight singleton. Cache it. +- **Sharing `ObjectContext` that change object state across threads.** Not safe. New context per thread. +- **Mixing objects across contexts.** A `Painting` loaded in context A cannot be assigned to an `Artist` in context B. Use `ctx.localObject(otherObject)` to copy across. +- **Forgetting `commitChanges()`.** Mutations to persistent objects stay in memory until commit. diff --git a/ai-plugin/skills/cayenne-cgen/SKILL.md b/ai-plugin/skills/cayenne-cgen/SKILL.md new file mode 100644 index 000000000..64c9a3133 --- /dev/null +++ b/ai-plugin/skills/cayenne-cgen/SKILL.md @@ -0,0 +1,77 @@ +--- +name: cayenne-cgen +description: "Use this skill whenever the user wants to (re)generate Cayenne entity Java classes from a DataMap. Trigger on phrases like 'generate Java classes', 'regenerate entities', 'run cgen', 'create the entity classes', 'why is the Artist class missing fields', 'where did the `_Abstract*` classes come from', 'sync the entity classes with the model', or any request to materialize Java from the DataMap. Also trigger as a follow-up after modeling changes (someone added an entity, attr [...] +--- + +# cayenne-cgen + +Run Cayenne's class generator on a DataMap via the `mcp__cayenne__cgen_run` MCP tool. Reads the embedded `<cgen>` block in the DataMap to determine destination, mode, templates, etc. + +## Required reading + +- `${CLAUDE_PLUGIN_ROOT}/references/mcp-tools.md` — `cgen_run` tool reference (arguments, return shape, failure modes). +- `${CLAUDE_PLUGIN_ROOT}/references/cgen-config.md` — every `<cgen>` field; needed when the user has to add or tweak the config block. +- `${CLAUDE_PLUGIN_ROOT}/references/project-layout.md` — locate the project descriptor. + +## Step 1 — Resolve project and DataMap + +The MCP tool needs two arguments: + +- `projectPath` — **absolute path** to the top-level project descriptor (`cayenne-*.xml`). **Not** a DataMap file. +- `dataMap` — the **name** as it appears in `<map name="...">` in the descriptor. **Not** a file path. + +Locate the descriptor via `project-layout.md`. If multiple descriptors exist, ask which one. Open the descriptor to extract the DataMap names from `<map>` elements. + +If the user named the DataMap directly (e.g. "regenerate classes for the customers DataMap"), use that. If they said "regenerate everything" and there are multiple DataMaps, run `cgen_run` once per DataMap in sequence (the tool generates per-DataMap). + +## Step 2 — Verify `<cgen>` block exists + +Before calling the tool, check the target DataMap for a `<cgen xmlns="http://cayenne.apache.org/schema/12/cgen">` block. If missing: + +1. Don't call `cgen_run` blindly — it will return an error. +2. Either: + - **Add a minimal block** by delegating to the `cayenne-modeling` skill. Use the starter config from `cgen-config.md`: + ```xml + <cgen xmlns="http://cayenne.apache.org/schema/12/cgen"> + <destDir>../java</destDir> + <mode>entity</mode> + <makePairs>true</makePairs> + <usePkgPath>true</usePkgPath> + <createPKProperties>true</createPKProperties> + </cgen> + ``` + Confirm `destDir` with the user — it's relative to the project XML directory and decides where generated `.java` files land. + - Or, if the user prefers the GUI, suggest the `cayenne-modeler` skill and tell them to configure the DataMap's "Class Generation" tab. + +## Step 3 — Call `cgen_run` + +``` +mcp__cayenne__cgen_run({ + "projectPath": "<absolute path to cayenne-*.xml>", + "dataMap": "<map name from the descriptor>" +}) +``` + +If the tool is not available (MCP server not registered), surface `cayenne-mcp-server/README.md` and stop. **Do not** suggest `mvn cayenne:cgen` or the Gradle cgen task. + +## Step 4 — Surface the result + +The tool returns structured JSON. Report: + +- `summary.writtenCount`, `summary.skippedCount`, `summary.errorCount` verbatim — these are the headline. +- The first few entries in `writtenFiles` (relative paths). Full list is informational; offer to dump it if the user asks. +- Any entries in `errors` — these are blocking. Read the messages and explain in user terms (a missing entity class name, a bad template path, an invalid `<destDir>`, etc.). + +If `writtenCount` is 0 and `skippedCount` covers everything, say so — it means everything is already up-to-date and no work was needed. + +## Step 5 — Next steps + +- If new `_<Entity>.java` superclass files were generated, gently remind the user not to edit those — they will be overwritten next run. User code goes in the matching `<Entity>.java` subclass. +- If the user just ran reverse engineering, this skill is the natural follow-up. Cross-link back to `cayenne-modeling` if they need to tweak names/types before regenerating. + +## Anti-patterns + +- **Do not** call `cgen_run` without verifying the `<cgen>` block exists — the tool returns an error and confuses the user. +- **Do not** suggest Maven (`mvn cayenne:cgen`) or Gradle (`cayenneCgen`) goals when MCP is unavailable. Those build plugins are out of scope. Point at MCP setup instead. +- **Do not** edit `_<Entity>.java` files. Generated superclasses. Edit `<Entity>.java` subclasses. +- **Do not** confuse `projectPath` and `dataMap` arguments. `projectPath` is a file system path to `cayenne-*.xml`. `dataMap` is a logical name (e.g. `mydb`), not a path to `mydb.map.xml`. diff --git a/ai-plugin/skills/cayenne-modeler/SKILL.md b/ai-plugin/skills/cayenne-modeler/SKILL.md new file mode 100644 index 000000000..76d52e3f1 --- /dev/null +++ b/ai-plugin/skills/cayenne-modeler/SKILL.md @@ -0,0 +1,61 @@ +--- +name: cayenne-modeler +description: "Use this skill when the user explicitly wants to open CayenneModeler (the GUI) on a Cayenne project, or when the modeling task is inherently visual — reverse engineering (delegated to cayenne-reverse-engineer), bulk relationship layout, multi-entity visual refactoring. Trigger on phrases like 'open the Modeler', 'open in CayenneModeler', 'launch the GUI', 'edit visually', 'show me the project in the Modeler'. Do NOT trigger as a fallback for ordinary a-la-carte XML edits — [...] +--- + +# cayenne-modeler + +Launch CayenneModeler with a Cayenne project file pre-loaded, via the `mcp__cayenne__open_project` MCP tool. + +## Required reading + +- `${CLAUDE_PLUGIN_ROOT}/references/mcp-tools.md` — `open_project` tool reference and failure modes. +- `${CLAUDE_PLUGIN_ROOT}/references/project-layout.md` — locate the project descriptor. + +## Step 1 — Resolve the project path + +The MCP tool needs an **absolute path** to a top-level project descriptor (`cayenne-*.xml`). Use `project-layout.md` to find it. If multiple descriptors exist, ask which one. Cache for the rest of the session. + +If no descriptor exists yet, the user is starting from scratch. Two options: + +1. Create a minimal descriptor first (use `cayenne-modeling` to scaffold it), then open. +2. Open the Modeler on *any* path it'll accept and use **File → New Project** inside. + +For starting fresh, option 2 is simpler — but `open_project` does require an existing readable file. + +## Step 2 — Call `open_project` + +``` +mcp__cayenne__open_project({ "projectPath": "<absolute path to cayenne-*.xml>" }) +``` + +If the tool is not available (MCP server not registered), surface `cayenne-mcp-server/README.md` and stop. Do not attempt to launch CayenneModeler by any other means — the bundled launcher relies on the MCP server's discovery of the Modeler installation. + +The tool spawns the Modeler asynchronously, then waits up to ~15 seconds for a startup handshake. Possible return codes: + +| Code | Meaning | +|---|---| +| `ok` | Modeler launched and loaded the project. | +| `modeler_not_found` | The MCP jar is not co-located with a Modeler installation. User needs to install CayenneModeler properly. | +| `project_not_found` | The `projectPath` doesn't exist or isn't readable. | +| `handshake_timeout` | The Modeler started but didn't confirm load within the timeout. Often this means it's still loading — the user can check the GUI directly. | +| `launch_failed` | Process spawn failed. Surface the error message. | + +## Step 3 — Hand off to the user + +Once `open_project` returns `ok`, the user is in the GUI. From here, depending on intent: + +- **Reverse engineering**: that's the `cayenne-reverse-engineer` skill's job to walk them through. Do not duplicate that workflow here — just open and step out. +- **Visual layout / bulk editing**: tell the user what tab to navigate to (e.g., DataMap → ObjEntity for entity-level edits, DataMap → Class Generation for cgen config) and let them work. Don't try to script GUI actions. +- **Just wanted to see the project**: nothing more to do. + +## When NOT to use this skill + +- **A-la-carte XML edits.** Adding one entity, tweaking one attribute, renaming a relationship — all faster as direct XML edits via `cayenne-modeling`. Don't context-switch the user to the GUI for trivial work. +- **Class generation.** That's `cayenne-cgen`'s job, fully scripted via MCP. No GUI needed. + +## Anti-patterns + +- **Do not** trigger as a generic fallback for "Cayenne tasks." This skill is for explicit user requests or inherently visual workflows. The `cayenne-modeling` skill handles most modeling intent. +- **Do not** retry `open_project` on `handshake_timeout` — the Modeler may still be loading. Tell the user to check the GUI window directly. +- **Do not** attempt to launch CayenneModeler outside the MCP tool (no `java -jar` direct calls, no opening a `.dmg`, no `open -a CayenneModeler`). The MCP tool's launcher knows where the installation lives. diff --git a/ai-plugin/skills/cayenne-modeling/SKILL.md b/ai-plugin/skills/cayenne-modeling/SKILL.md new file mode 100644 index 000000000..91735e8e0 --- /dev/null +++ b/ai-plugin/skills/cayenne-modeling/SKILL.md @@ -0,0 +1,74 @@ +--- +name: cayenne-modeling +description: "Use this skill whenever the user wants to edit, inspect, or extend the Cayenne ORM model in a project — adding or modifying entities, attributes, relationships, embeddables, named queries, stored procedures, or DataNodes. Trigger on phrases like 'add an ObjEntity', 'add a DbEntity', 'add a relationship', 'expose this column as an attribute', 'create a new DataMap', 'add a named query', 'create an embeddable', 'add a stored procedure', 'change the attribute type', 'mark this [...] +--- + +# cayenne-modeling + +Edit Cayenne DataMap (`*.map.xml`) and project descriptor (`cayenne-*.xml`) files directly. This is the **default path** for granular model changes — adding one entity, one relationship, one query. Use the Modeler GUI (`cayenne-modeler` skill) only for inherently visual work like reverse engineering. + +## Required reading + +Read these before making any edit — they describe the XML formats this skill operates on: + +- `${CLAUDE_PLUGIN_ROOT}/references/project-layout.md` — locate the project descriptor and DataMaps in the user's repo. +- `${CLAUDE_PLUGIN_ROOT}/references/datamap-schema.md` — every element shape for `*.map.xml`. +- `${CLAUDE_PLUGIN_ROOT}/references/project-descriptor-schema.md` — `cayenne-*.xml` shape. + +## Step 1 — Locate the right file + +Follow `project-layout.md`. If the user has multiple Cayenne projects and the request is ambiguous, ask which one. Cache the answer for the rest of the session. + +Decide whether the change belongs in a DataMap or the project descriptor (table at the end of `project-layout.md`): + +- Entity/relationship/query/embeddable/procedure → DataMap (`*.map.xml`). +- DataMap list, DataNode (DB connection) → project descriptor (`cayenne-*.xml`). + +## Step 2 — Make the edit + +Apply the change following the schema in `datamap-schema.md` or `project-descriptor-schema.md`. + +### Critical rules + +1. **Element order matters.** The DataMap schema requires this order inside `<data-map>`: `<property>`, `<procedure>`, `<embeddable>`, `<db-entity>`, `<obj-entity>`, `<db-relationship>`, `<obj-relationship>`, `<query>`, `<cgen>`, `<dbImport>`. Insert at the right place — don't append blindly. + +2. **Cross-link consistently.** + - An `<obj-entity>` references a `<db-entity>` by `dbEntityName="..."`. Make sure that DbEntity exists. + - An `<obj-attribute>` references a DbAttribute by `db-attribute-path="COLUMN_NAME"`. Make sure that column exists on the DbEntity (or on a related one if using a dotted path through a relationship). + - An `<obj-relationship>` requires a backing `<db-relationship>` (or chain of them) via `db-relationship-path`. Never add an ObjRelationship without the DB-layer counterpart. + - Most FKs need **two** `<db-relationship>` entries, one per direction. They are not auto-derived. + +3. **Use the right `type` on attributes.** + - `db-attribute type` — JDBC type names: `VARCHAR`, `INTEGER`, `BIGINT`, `DATE`, `TIMESTAMP`, `BOOLEAN`, `NUMERIC`, `BLOB`, `CLOB`, `VARBINARY`, etc. VARCHAR/CHAR/VARBINARY also need `length`. NUMERIC/DECIMAL also need `scale`. + - `obj-attribute type` — Java FQN: `java.lang.String`, `java.lang.Integer`, `java.util.Date`, `java.math.BigDecimal`, `byte[]`, etc. Use wrapper types (`Integer`, not `int`) for nullable columns. + +4. **PK handling.** PK columns get `isPrimaryKey="true" isMandatory="true"` on the DbAttribute. They are normally **not** mirrored as ObjAttributes — Cayenne handles them implicitly. Map a PK as an ObjAttribute only if the user wants a "meaningful PK" (visible on the Java side). + +5. **Preserve formatting.** Match the indentation and quote style of the file (the standard Cayenne style uses tabs and double quotes, but follow what's actually in the file you're editing). + +## Step 3 — Validate the edit conceptually + +After writing the XML, mentally walk through: + +- Does every `obj-entity.dbEntityName` resolve to an existing `<db-entity>`? +- Does every `obj-attribute.db-attribute-path` resolve to a column on the right DbEntity? +- Does every `obj-relationship.db-relationship-path` resolve to a chain of existing `<db-relationship>` entries? +- Are PK and FK columns marked `isMandatory="true"` where the DB enforces NOT NULL? + +If anything fails, fix it before reporting done. + +## Step 4 — Offer the right next step + +- **If you modified entities and the DataMap has a `<cgen>` block:** suggest invoking the `cayenne-cgen` skill to regenerate Java classes. Mention which entities are affected. +- **If the user added a new entity and there's no Java class yet:** same — recommend `cayenne-cgen`. +- **If the user is asking about a full DB sync** (importing many tables, syncing with a changed schema): hand off to `cayenne-reverse-engineer`. Do not try to script this via XML edits. +- **If the change is structurally messy** (bulk renaming relationships, visual graph rework): suggest the `cayenne-modeler` skill. Otherwise do not. + +## Anti-patterns + +- **Don't hand-edit `_<Entity>.java` superclass files.** They are regenerated by cgen and your changes will be overwritten. Edit the user `<Entity>.java` subclass instead. +- **Don't add an `obj-relationship` without a `db-relationship`.** Cayenne will validate-fail at runtime. +- **Don't use primitive Java types (`int`, `long`, `boolean`) as `obj-attribute type` for nullable columns.** Primitives can't represent NULL. Use wrappers. +- **Don't reorder existing elements.** The schema requires the order documented above; reordering existing elements may also create noisy diffs. +- **Don't run cgen yourself.** That's `cayenne-cgen`'s job — invoke that skill instead of calling the MCP tool directly here. +- **Don't suggest `mvn cayenne:cdbimport` or any Maven/Gradle plugin goal.** Those are explicitly out of scope for this plugin. diff --git a/ai-plugin/skills/cayenne-query/SKILL.md b/ai-plugin/skills/cayenne-query/SKILL.md new file mode 100644 index 000000000..ef3c98ed7 --- /dev/null +++ b/ai-plugin/skills/cayenne-query/SKILL.md @@ -0,0 +1,105 @@ +--- +name: cayenne-query +description: "Use this skill whenever the user wants to write or modify a Cayenne query — fetching entities by criteria, joining, prefetching to avoid N+1, ordering, paginating, aggregating, or running raw SQL through Cayenne. Trigger on phrases like 'query for X', 'fetch all artists where ...', 'write an ObjectSelect', 'use SQLSelect', 'use SelectById', 'add a prefetch', 'get distinct values', 'count rows', 'find by ID', 'load by primary key', 'build a Cayenne expression', 'why am I get [...] +--- + +# cayenne-query + +Write idiomatic Cayenne 5.0 queries — `ObjectSelect`, `SelectById`, `SQLSelect`, expressions, prefetch, pagination, aggregates. + +## Required reading + +- `${CLAUDE_PLUGIN_ROOT}/references/query-api.md` — every query mechanism with examples (ObjectSelect, SQLSelect/SQLExec, Expression, ColumnSelect, SelectById). + +## Step 1 — Identify the query shape + +| Intent | Use | +|---|---| +| Fetch one or more entities matching criteria | `ObjectSelect.query(Cls.class).where(...).select(ctx)` | +| Fetch by primary key | `SelectById.query(Cls.class, pk).selectOne(ctx)` | +| Aggregate (count, sum) | `ObjectSelect.query(Cls.class).selectCount(ctx)` or `ColumnSelect` with aggregate functions | +| One or a few columns only (DTO-style) | `ObjectSelect.columnQuery(Cls.class, Cls.NAME, Cls.AGE).select(ctx)` | +| Raw SQL with parameter binding | `SQLSelect.query(Cls.class, "SELECT ...").params(...).select(ctx)` | +| Insert/update/delete bulk | `SQLExec.query("UPDATE ...").update(ctx)` | +| Reused named query stored in DataMap | XML `<query>` (see `${CLAUDE_PLUGIN_ROOT}/references/datamap-schema.md`) loaded via `NamedQuery` | + +### Primary-key lookups: prefer `SelectById` + +When the user is fetching a single entity by its PK, use `SelectById` rather than `ObjectSelect.where(PK.eq(...))`: + +```java +Artist a = SelectById.query(Artist.class, 42).selectOne(ctx); +``` + +`SelectById` can hit the `ObjectContext`'s session cache without firing a SQL query when the row is already loaded. `ObjectSelect.where(...)` always goes to the database. For composite PKs, pass a `Map<String, Object>` of PK column → value. + +## Step 2 — Filter with `Property` constants + +cgen generates `Property<T>` constants on each entity superclass. Use them — they're type-checked at compile time: + +```java +ObjectSelect.query(Artist.class) + .where(Artist.ARTIST_NAME.likeIgnoreCase("p%")) + .and(Artist.DATE_OF_BIRTH.between(d1, d2)) + .select(ctx); +``` + +Only fall back to `ExpressionFactory.matchExp("artistName", ...)` or `Expression.fromString(...)` when: + +- The filter is dynamic (field name comes from runtime input), or +- The user explicitly wants string-based expressions. + +`query-api.md` lists all common predicate methods on `Property`. + +## Step 3 — Handle relationships (prefetch) + +When the query traverses a relationship in a loop, **always** add a prefetch to avoid N+1: + +```java +// Bad — fires one query per artist when paintings are accessed +for (Artist a : artists) { a.getPaintings().size(); } + +// Good +List<Artist> artists = ObjectSelect.query(Artist.class) + .prefetch(Artist.PAINTINGS.disjoint()) + .select(ctx); +``` + +Pick: + +- `.joint()` — to-one relationships, or to-many with small fan-out. Single SQL join. +- `.disjoint()` — to-many, modest size. Two queries. +- `.disjointById()` — to-many, very large parent set. Two queries, keyed by PKs. + +## Step 4 — Apply ordering, paging, and caching as needed + +```java +ObjectSelect.query(Artist.class) + .orderBy(Artist.ARTIST_NAME.asc()) + .pageSize(50) // server-side pagination + .limit(1000) + .localCache() // per-context cache, or .sharedCache("group-name") + .select(ctx); +``` + +Use `sharedCache` for reference data (rarely changes, read often). Use `pageSize` to avoid loading entire result sets. + +## Step 5 — Raw SQL when needed + +```java +List<Artist> hits = SQLSelect.query(Artist.class, + "SELECT * FROM ARTIST WHERE ARTIST_NAME LIKE #bind($pattern)") + .params(Map.of("pattern", userInput + "%")) + .select(ctx); +``` + +**Always** use `#bind($name)` placeholders. Never concatenate user input into SQL. Cayenne's SQLTemplate is Velocity-based — see `query-api.md` for `#bind`, `#bindEqual`, `#chain`, and adapter-specific SQL with `<sql adapter-class="...">`. + +## Anti-patterns + +- **Parameter String concatenation in raw SQL.** Use `#bind($name)`. Concatenation is a SQL injection vector and may also result in invalid syntax. +- **Using `selectOne` when multiple may match.** It throws. Use `selectFirst` if "any one" is okay. +- **Loading large result sets without pagination.** Use `.pageSize(n)` or `iterator()` and process incrementally. +- **N+1 from missing prefetch.** If iterating entities and accessing relationships per-entity, add `.prefetch(...)`. +- **Using `Expression.fromString(...)` or `ExpressionFactory.matchExp("fieldName", ...)` for static queries.** Prefer typed `Property` constants (`Artist.ARTIST_NAME.eq(...)`) — they catch typos at compile time and survive model refactors. +- **Mutating fetched objects without committing.** `ObjectContext.commitChanges()` is required for changes to persist. diff --git a/ai-plugin/skills/cayenne-reverse-engineer/SKILL.md b/ai-plugin/skills/cayenne-reverse-engineer/SKILL.md new file mode 100644 index 000000000..4f22c628a --- /dev/null +++ b/ai-plugin/skills/cayenne-reverse-engineer/SKILL.md @@ -0,0 +1,80 @@ +--- +name: cayenne-reverse-engineer +description: "Use this skill whenever the user wants to import database schema metadata into a Cayenne DataMap — full-schema sync from a live DB. Trigger on phrases like 'reverse engineer the database', 'import the schema', 'generate a DataMap from my DB', 'sync the model with the database', 'add the new tables from the DB', 'import the customer table', 'pick up the latest schema changes', 'create entities from these tables', or any request that involves reading database metadata to popu [...] +--- + +# cayenne-reverse-engineer + +Import a database schema into a Cayenne DataMap by driving the CayenneModeler's reverse-engineering wizard through MCP. Cayenne 5.0 does not yet expose reverse engineering as a direct MCP tool, so the workflow launches the GUI and walks the user through the wizard. + +## Required reading + +- `${CLAUDE_PLUGIN_ROOT}/references/project-layout.md` — locate or create the project descriptor. +- `${CLAUDE_PLUGIN_ROOT}/references/dbimport-config.md` — the field semantics behind every wizard screen, so you can explain options in user terms. +- `${CLAUDE_PLUGIN_ROOT}/references/mcp-tools.md` — `open_project` tool reference and behavior when MCP is not connected. + +## Step 1 — Confirm scope + +Ask the user (one question, only if not obvious from the request): + +- Are they reverse-engineering into a **new DataMap**, or **adding/updating** entities in an **existing DataMap**? + +## Step 2 — Locate or create the project + +Follow `project-layout.md` to find the existing `cayenne-*.xml`. If none exists, the user is starting from scratch: + +- Either generate a minimal descriptor first (use `cayenne-modeling`'s patterns — namespace `http://cayenne.apache.org/schema/12/domain`, `project-version="12"`, one empty `<map name="..."/>` plus a sibling empty `*.map.xml`), or +- Tell the user to use **File → New Project** inside the Modeler once it's open. + +The descriptor path needs to be **absolute** when passed to `open_project`. + +## Step 3 — Launch the Modeler + +Call the MCP tool: + +``` +mcp__cayenne__open_project({ "projectPath": "<absolute path to cayenne-*.xml>" }) +``` + +If the tool is not available (server not registered), surface `cayenne-mcp-server/README.md` and stop. **Do not** suggest `mvn cayenne:cdbimport` or any Gradle equivalent — those build plugins are out of scope. + +If `open_project` returns a non-`ok` status, surface the error code and message; common ones are `modeler_not_found`, `project_not_found`, `handshake_timeout`. Don't retry blindly — diagnose first. + +## Step 4 — Walk the user through the wizard + +The user now has the Modeler open. Give them the exact GUI sequence: + +1. **DataMap → DB Import tab** +2. **Data Source** dialog: + - **JDBC Driver** — e.g., `org.postgresql.Driver` + - **DB URL** — full JDBC URL + - **User Name / Password** — credentials + - **Cayenne Adapter** (optional, autodetected for known DBs) — e.g., `org.apache.cayenne.dba.postgres.PostgresAdapter` + - Click **Test Connection**, then **Continue**. +3. **Configure** screen — set table/column/procedure filters: + - Include/exclude regex patterns under the relevant catalog/schema. + - **Table Types** — usually leave at `TABLE`; add `VIEW` if the user wants views imported. +4. **Naming** screen: + - **Naming Strategy** — usually the default (`DefaultObjectNameGenerator`). + - **Strip from Table Names** — regex to strip a prefix like `^TBL_`. + - **Default Package** — Java package for generated ObjEntity classes. + - **Meaningful PK Tables** — regex (or `*`) for tables whose PK columns should be exposed as ObjAttributes. Leave empty unless the user needs PK visibility. +5. **Other Options** — toggles for `skipPrimaryKeyLoading`, `skipRelationshipsLoading`, `forceDataMapCatalog`, `forceDataMapSchema`, `useJava7Types`. See `dbimport-config.md` for what each does; defaults are usually right. +6. Click **Save** then **Run Import**. The Modeler runs the import and reports a diff (added/changed/removed entities). + +Explain options as the user asks — `dbimport-config.md` has the semantics. When unsure, recommend the default. + +## Step 5 — Confirm and follow up + +Once the user reports the import is done and they've saved the project: + +- Tell them to **save the project** in the Modeler (File → Save). The Modeler persists wizard settings as a `<dbImport>` block inside the DataMap for repeat runs. +- 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`. +- If the DB has columns that don't follow the user's preferred naming, recommend tweaking the naming strategy and re-running. + +## Anti-patterns + +- **Do not** suggest `mvn cayenne:cdbimport`, the Gradle `cdbimport` task, or hand-running the `cayenne-dbsync` Java APIs. The Modeler GUI is the only supported execution path here. +- **Do not** try to hand-write a DataMap from a DB schema description as a substitute for the wizard — the wizard handles JDBC types, PK detection, FK relationships, and naming consistently. Hand-rolling produces subtle mistakes. +- **Do not** enable `forceDataMapCatalog` / `forceDataMapSchema` defensively. They suppress legitimate DB metadata and cause hard-to-debug issues in multi-catalog setups. +- **Do not** offer reverse engineering without MCP — if the server isn't connected, point at `cayenne-mcp-server/README.md` and stop. diff --git a/ai-plugin/skills/cayenne-runtime/SKILL.md b/ai-plugin/skills/cayenne-runtime/SKILL.md new file mode 100644 index 000000000..e46ccfadc --- /dev/null +++ b/ai-plugin/skills/cayenne-runtime/SKILL.md @@ -0,0 +1,86 @@ +--- +name: cayenne-runtime +description: "Use this skill whenever the user wants to bootstrap Cayenne in a Java application — constructing a CayenneRuntime, wiring a DataSource, configuring connection pools, creating ObjectContexts, or integrating Cayenne with a DI container (Bootique, Spring, plain Java). Trigger on phrases like 'set up Cayenne in my app', 'configure CayenneRuntime', 'wire Cayenne into Bootique/Spring', 'create an ObjectContext', 'configure a DataSource for Cayenne', 'connect Cayenne to Postgres/M [...] +--- + +# cayenne-runtime + +Bootstrap `CayenneRuntime` and use `ObjectContext` for CRUD in a Java application. + +## Required reading + +- `${CLAUDE_PLUGIN_ROOT}/references/runtime-api.md` — `CayenneRuntimeBuilder` methods, `ObjectContext` API, transaction patterns, lifecycle rules. + +## Step 1 — Find or create the bootstrap site + +Where does the user's app construct `CayenneRuntime`? Common patterns: + +- **Plain Java / CLI** — a `main()` method or a singleton holder. +- **Bootique app** — typically a `@Provides` method in a module (`provideCayenneRuntime(@Inject DataSource ds)`). If editing a Bootique app, mention that the `java-dev:bootique-config` skill (if available) can help with the surrounding config wiring. +- **Spring app** — a `@Bean CayenneRuntime cayenneRuntime(DataSource ds)` method in a configuration class. +- **Cayenne-only embedded** — a static field or DI singleton initialized lazily. + +If none exists yet, create one. Apply `runtime-api.md` patterns: + +```java +CayenneRuntime runtime = CayenneRuntime.builder() + .addConfig("cayenne-mydb.xml") + .dataSource(externalDataSource) // or .url(...).jdbcDriver(...).user(...).password(...) + .build(); +``` + +## Step 2 — Decide on the DataSource path + +Two paths from `CayenneRuntimeBuilder`: + +| When | Use | +|---|---| +| Production app with Spring/Bootique/Hikari already managing the pool | `.dataSource(ds)` — Cayenne reuses the external pool. | +| Standalone tool or test fixture with no external DI | `.url(...).jdbcDriver(...).user(...).password(...)` — Cayenne's built-in pool. | + +The `.dataSource()` path is preferred when possible. Never put JDBC credentials in `cayenne-*.xml` for production code — keep them in the application's normal config mechanism. + +## Step 3 — Use `ObjectContext` + +```java +ObjectContext ctx = runtime.newContext(); // one per request/thread +Artist a = ctx.newObject(Artist.class); +a.setArtistName("Picasso"); +ctx.commitChanges(); +``` + +Rules: + +- `CayenneRuntime` is a heavy singleton — **create once, share for app lifetime**, call `shutdown()` on exit. +- `ObjectContext` is **not thread-safe** — new one per request/thread. +- Don't mix objects across contexts. Use `ctx.localObject(other)` to bring an object into the current context. + +## Step 4 — Transactions + +`commitChanges()` is auto-transactional. For multi-step transactions: + +```java +runtime.performInTransaction(() -> { + // multiple commits in one transaction + return null; +}); +``` + +Any throw rolls back the whole block. + +## Step 5 — Verify the integration + +Tell the user how to smoke-test: + +1. Build and run the app. +2. From a request handler: `ObjectContext ctx = runtime.newContext(); long count = ObjectSelect.query(SomeEntity.class).selectCount(ctx);` — should return a number without throwing. + +If they hit `CayenneRuntimeException: No DataNode...`, the descriptor's `<map>` reference doesn't have a node — confirm the DataSource wiring went through `CayenneRuntimeBuilder.dataSource(...)` (Cayenne synthesizes a node from this). + +## Anti-patterns + +- **Creating `CayenneRuntime` per request.** It's a heavyweight singleton. Cache it for the application lifetime. +- **Sharing `ObjectContext` across threads.** Not safe. Create one per thread. +- **Putting DB credentials in `cayenne-*.xml`** for production. Use the application's secret management; pass a `DataSource` to the builder. +- **Forgetting `commitChanges()`.** Mutations stay in memory until commit. +- **Forgetting `shutdown()`** on app exit. Hangs the JVM on some pool implementations.
