SQL Maven Plugin
The Tentackle SQL Maven Plugin¶
The tentackle-sql-maven-plugin turns the Tentackle entity model
into database DDL. Where the tentackle-maven-plugin
generates the Java- and service metadata of an application, this plugin is responsible for everything
that touches the database schema:
- generating a
CREATE TABLEscript for a fresh database, - generating an incremental migration script by comparing the model against the metadata of an existing database,
- validating that a database is still in sync with the model,
- running arbitrary SQL scripts against the configured backend(s), and
- rendering the model as a set of cross-linked HTML pages for browsing.
All goals derive the schema from the same model and emit backend-specific SQL through Tentackle's
org.tentackle.sql.Backend abstraction, so the very same model can be materialized for PostgreSQL,
MySQL/MariaDB, Oracle, H2, and the other supported backends without changing a single model file.
The goals share the prefix tentackle-sql, so they can be invoked directly from the command line,
e.g. mvn tentackle-sql:create.
Prerequisites¶
The plugin requires the same JDK and Maven versions as the rest of the framework
(JDK >= 25, Maven >= 3.9.10, Maven 4 is supported as well). The migrate, validate, run and reveng goals connect to a live
database and therefore need a reachable backend and the matching JDBC driver on the plugin's
classpath (add it to the plugin's <dependencies>). For reveng that backend is typically the
existing database being reverse-engineered, not necessarily the application's own.
Where the Model Comes From¶
None of the goals parse Java sources directly. They read the model — the entity definitions that the
wurbelizer extracts from the PDO interfaces during code generation and writes to
${project.build.directory}/wurbel/model (one file per entity). This is the default value of the
modelDir parameter, so in a normal Tentackle build the SQL plugin just picks up what the wurbelizer
already produced earlier in the same build. You can point it elsewhere with modelDir or override the
selection entirely with explicit filesets.
The one exception is reveng (see below): it goes the other way, reading an
existing database's live schema and writing model source text for tentackle-wizard:pdo to
consume — it never reads modelDir/filesets and has nothing to do with the model of the project
it runs in.
Because all goals are aggregators (they operate on the whole reactor at once), they are typically run on the root project rather than per module.
Overview of Goals¶
| Goal | Aggregator | Connects to DB | Purpose |
|---|---|---|---|
create |
yes | no | Generate a CREATE TABLE script (plus indexes, foreign keys, schemas, sequences) for the whole model. |
migrate |
yes | yes | Compare the model against the live database metadata and generate an incremental migration script. |
validate |
yes | yes | Like migrate with validate=true: fail the build if the database differs from the model. |
run |
yes | yes | Execute an arbitrary SQL script against the configured backend(s). |
document |
yes | no | Render the model as cross-linked, backend-specific HTML or Markdown pages, one per entity, plus an index page. |
reveng |
yes | yes | The reverse direction: turn tables of an existing database into model source for tentackle-wizard:pdo. |
help |
— | no | Print plugin usage information (generated by the maven-plugin-plugin). |
The generated scripts are written per backend to ${project.build.directory}/sql/<backend-name>/,
e.g. target/sql/postgres/createmodel.sql. document follows the same per-backend layout, writing to
${project.build.directory}/model-doc/<backend-name>/.
Common Configuration¶
Like all Tentackle mojos, the goals inherit the common parameters verbosity, skip, charset,
jdkToolchain and minLogLevel (see the tentackle-maven-plugin docs).
On top of that, AbstractSqlMojo adds the parameters shared by all goals except reveng, which is
configured separately (see below).
sqlDir(tentackle.sqlDir): the base directory of the generated scripts, one sub-directory per backend. Defaults to${project.build.directory}/sql.generateHeaderComment(defaulttrue): start each generated script with a comment naming the model location, the creation time, the user and host, and the backend. Formigrate, it also enables the statistics and the list of unexpected tables at the top of the script.
All goals are thread-safe. The model, however, is held in a JVM-wide singleton, so in a parallel build
(mvn -T …) concurrent executions are serialized, except for reveng; see
Parallel builds.
Model selection¶
modelDir(tentackle.modelDir): directory holding the model files. Defaults to${project.build.directory}/wurbel/model. Ignored iffilesetsis given.filesets: explicit file sets to process instead ofmodelDir.modelName(tentackle.modelName): the model to use, for the rare case that a project defines more than one. Defaults to the standard model.modelDefaults(tentackle.modelDefaults): model defaults applied to every entity (same syntax as the[defaults ...]block in a model file).entityAliases(tentackle.entityAliases): entity alias definitions.mapSchemas(tentackle.mapSchemas): map schema-qualified table names to flat table names, for backends that do not support schemas (Oracle, MySQL, MariaDB, Firebird, Informix). Must match themapSchemawurblet property that configures the runtime side — see Table Names.
Backend selection¶
The set of backends to generate SQL for can be specified in three ways, which may be combined. Later definitions override earlier ones for the same backend name:
- By name via
backendNames(tentackle.backendNames) — a comma-separated list of backend names. The special valuesall(all non-deprecated backends),noneanddeprecatedare recognized. This is the usual choice for thecreateanddocumentgoals, which do not need a connection. - From property files via
backendProperties— file sets ofEncryptedPropertiesfiles that describe a backend including its JDBC URL and credentials. If no<directory>is given, the project's resource directories are scanned. These backends are connectable. - Inline in the pom via
backends— a list of<backend>elements (see below). Also connectable.
A connectable backend is required for migrate, validate and run; create and document work
with name-only backends because they never touch a database. The goals that connect process each
connectable backend separately, keyed by its JDBC URL — two databases of the same backend type are
both processed. create and document generate one script or page tree per backend name instead.
<backends>
<backend>
<url>jdbc:postgresql://localhost/muz</url>
<user>muz</user>
<password>secret</password>
<schemaNames>muz, tx</schemaNames> <!-- or a <schemas> list of <schema> elements -->
</backend>
</backends>
Instead of an inline <user>/<password> you can reference a <serverId> from settings.xml; the
plugin then reads the (optionally encrypted) credentials from the Maven settings, replacing any inline
ones. An unknown <serverId> fails the build.
The schemas restrict migrate and validate to the entities whose tables live in one of the listed
schemas (the others are counted as skipped) and to the unexpected tables of those schemas. Without
schemas, all entities are migrated. The <migrationHints> and <minVersion> elements configure the
migration hints and their version filtering (see below). They exist only for <backends> entries:
backends from backendProperties are migrated without hints.
Name checks¶
checkModelNames(defaulttrue): check the entity, attribute, relation and index names of the model with theNameVerifier(application naming conventions on top of the backend rules). Turn it off to load legacy models whose names don't follow the rules.checkReservedTables(defaulttrue): assert that no model table reuses a name reserved by the Tentackle framework.checkBackendSpecificNames(defaulttrue): assert that column, schema, table, and index names do not collide with reserved words of the configured backends.alienTables: a list of regular expressions matching tables that exist in the database but do not belong to the model (without schema prefix). They are excluded from the "unexpected tables" reporting and must not be claimed by a model entity: if one is,createandmigratefail.
Model errors are counted, and a goal with errors fails with <n> model errors after the script has
been written, so the output can still be inspected. A model that uses deferred or deferrable
constraints for a backend that does not support them only logs a warning: the DDL falls back to
NOT DEFERRABLE. At the end, create, migrate and validate warn about entities with
numbered fallback table aliases ("consider specifying explicit table aliases"), since such aliases
may change as the model evolves.
Diagnostics and dumps¶
These parameters help inspect what the plugin understood from the model — useful for debugging and
for packaging a model so downstream projects can build on it. They are honored by create,
migrate and validate only (and not by migrate with loadModelFromClassPath):
dumpDir: dump every parsed entity (plus_aliases_.txt,_tables_.txt,_classid_.txtindex files). Entities are dumped as<Entity>.txt, or as<Entity>.mapif neitherdumpAsCommentnordumpVariablesis set. The formatting is controlled by:dumpCompact(defaultfalse): print only the configuration keywords that are actually required, instead of all of them for proofreading;dumpAsComment(defaulttrue): dump the entities as comment blocks;dumpVariables(defaulttrue): include the model variables;dumpColumnGap(default2): the minimum number of spaces between the columns of the attribute section;dumpAnnotationsAsOptions: comma-separated annotations to print as attribute options, e.g.@NotNull|, @NotZero. A trailing|matches only annotations without parameters;@*matches all annotations.
dumpModelSource: dump the original model source instead of the parsed entity.dumpModelIndex: also write an index file listing the dumped files — combined withdumpModelSourcethis produces aMETA-INF/MODEL-INDEX.LISTso the model can be loaded from a jar (seeprependModelFromClassPath/loadModelFromClassPath).bundleDir: write*.propertiesbundles containing the attribute- and relation comments, ready to copy into the project for auto table configuration or default editors.
The create Goal¶
Generates a script that creates the whole schema from scratch. For every table-providing entity it
emits, in order: the CREATE SCHEMA (once per schema, unless mapSchemas or createSchemas=false),
the CREATE TABLE, and its indexes. The foreign-key constraints for the generated entities follow
after all tables. The complete script is laid out as:
- the
prependFilesets, - the classpath model, if
prependModelFromClassPath, - the header comment, the tables and the foreign keys of the model,
- the classpath model, if
appendModelFromClassPath, - the
sequences, - the
appendFilesets.
The default output file is createmodel.sql (parameter sqlFileName, property tentackle.createSqlFile). Important parameters:
createSchemas(defaulttrue): emitCREATE SCHEMAstatements for schema-qualified tables. If a backend does not support schemas, setmapSchemasinstead.sequences: a list of<sequence>elements (name,comment,start,increment) to create. Backends that do not support sequences get a commented-out note instead.prependFilesets/appendFilesets: file sets whose content is copied verbatim to the beginning or end of the generated script (e.g., for grants or seed data). Without a<directory>, the includes are relative to the project's base directory.prependModelFromClassPath/appendModelFromClassPath: also generate the tables for a model found on the classpath, e.g., the Tentackle framework's own tables. Requires the dependency to ship its model resources (built withdumpModelSource+dumpModelIndex).
The migrate Goal¶
Connects to each connectable backend, reads its metadata via JDBC, compares it table by table against the model, and generates the SQL needed to bring the database in line: adding/altering/dropping columns and indexes, renaming columns whose data should be preserved, adjusting foreign keys, and renaming or dropping tables where migration hints say so. If nothing differs, the script just records that no migration is necessary.
How columns are matched¶
The migrator preserves data wherever it can. Columns that exist under the same name are altered in
place. For the rest — attributes the model added and columns the database still has — it looks for
pairs and generates a RENAME (or a combined rename and type change) instead of a drop plus an add:
- an unambiguous match by SQL type: exactly one disappeared column has the type of the added attribute;
- otherwise the best match by syllable distance among the candidates of the same type, starting
with the longest names, which is why bulk renames such as
x_position → position_x,y_position → position_yandz_position → position_zare usually paired correctly in one pass.
Wherever the migrator guesses, it writes the alternative ADD and DROP statements into the script as
comments, so a wrong guess can be corrected instead of silently applied. Only columns left without a
counterpart become a real DROP or ADD. Type changes a backend cannot perform in place are likewise
emitted as a comment rather than as a statement that would fail or lose data.
Table renames, by contrast, are never guessed — they need a rename hint.
Tables that exist in the database but neither belong to the model nor match alienTables are
unexpected tables. Tables outside the backend's schemas or in its reserved schemas, and with
checkReservedTables also the framework's own tables, don't count.
The goal generates, it does not apply: the script is written to
${project.build.directory}/sql/<backend>/migratemodel.sql, one per backend, and executing it is a
separate step. The intended lifecycle is to review the generated script, commit it to the
repository as a versioned migration, and let a migration tool such as
Flyway or Liquibase apply it to the databases it manages;
tentackle-sql:run is for trying it out locally, not for rolling it out. Note also that a necessary
migration is only a warning — the build stays green unless validate is set.
The default output file is migratemodel.sql (parameter sqlFileName, property tentackle.migrateSqlFile). Important parameters:
validate: fail the build if any migration would be required (see thevalidategoal). It has no user property, so-Dvalidate=truedoes nothing; use thevalidategoal on the command line.failOnUnexpectedTables(defaultfalse): fail if the database contains unexpected tables. The script then lists them asDROP TABLEstatements. Whenfalse, they are only listed as a comment with the suggestion to add them to<alienTables>or remove them.loadModelFromClassPath(tentackle.loadModelFromClassPath): migrate the model found on the classpath instead of the local model files (e.g., to migrate only the framework's tables). When set, the model files in the file sets are ignored; usually combined withcheckReservedTables=false.useDropIfExists: useDROP ... IF EXISTSfor dropping indexes, constraints, and columns where the backend supports it (a warning is logged otherwise). Applies tomigrateandvalidateonly.split: write one file per migrated table into numbered sub-folders instead of a single script — handy when the migrations are post-processed by shell scripts. See Script layout.asTemplate(tentackle.migrateSqlAsTemplate): treat the generated migration as a FreeMarker template. The migration is first written tomigratemodel-sql.ftl(the file name with dots replaced by dashes), which is then rendered intomigratemodel.sqland removed. Withsplit, each file is rendered separately. The template sees:migrate.<EntityName>:trueif that entity's table needs migration,falseif not (only defined for the processed entities),url: the backend's JDBC URL,- all system and Maven properties, with their keys converted to camelCase (
os.name→osName).
This way, the final SQL can be tailored to what actually changed, e.g. recreate a view only if one of its tables was migrated:
Script layout¶
The generated migratemodel.sql consists of, in this order:
- the header comment, the statistics (tables per schema, processed/skipped entities) and the list of
unexpected tables (with
generateHeaderCommentonly), - the
always beforehints, - if anything has to be migrated: the
before allhints, the table migrations, the foreign keys and theafter allhints — or otherwise (withgenerateHeaderComment) a comment that no migration is necessary, - the
always afterhints, - the
DROP TABLEstatements of thedrophints ("tables removed from the model"), - a
WARNINGSsection with the optional migrations the migrator commented out.
With split, the main script only keeps items 1, 5 and 6. The rest goes into numbered sub-folders of
the backend's SQL directory, zero-padded to the same width:
| Folder | File(s) |
|---|---|
0 |
_always_before.sql, _before_all.sql |
1 … n |
<table>.sql, one per migrated table |
| next | _foreign_keys.sql, if any foreign key changed |
| next | _after_all.sql |
| highest | _always_after.sql |
As in the single script, before all, after all and the foreign keys only appear if anything
has to be migrated.
Migration hints¶
The automatic migrator handles structural changes that can be derived mechanically — including column renames, see above — but some changes need human guidance: renaming or dropping a table, converting the data when a column changes its type (a string becoming a number, say), correcting a rename the migrator guessed wrong, ordering migrations that depend on each other, choosing between alternative automatic migrations, or injecting handwritten SQL. This is what migration hints are for.
Hints are plain text files supplied per backend via the <migrationHints> file sets inside a
<backend>. Without a <directory>, the project's resource directories are searched. Each file is a
sequence of sections; a section starts at the beginning of a line with a keyword followed by a colon
and extends until the next keyword. Text before the first keyword is ignored:
-- rename the customer table and one of its columns, then add hand-written SQL
rename kunde: customer
rename customer#kdnr: customer_no
before customer:
ALTER TABLE customer ADD COLUMN migrated_at timestamp;
depend invoice: customer
Recognized keywords:
| Keyword | Effect |
|---|---|
always before / always after |
SQL emitted unconditionally before/after everything else. |
before all / after all |
SQL emitted before/after everything else, only if something changed. |
before <table> / after <table> |
SQL emitted before/after the migration of that table (if changed). |
migrate <table> |
Hand-written SQL that replaces the automatic migration of that table (if changed). |
migrate <table>#<col>[/<newcol>] |
Hand-written SQL for migrating a specific column (if changed). |
rename <table>: <newtable> |
Rename a table (same schema) when the old name exists in the DB and the new one is in the model. |
rename <table>#<col>: <newcol> |
Rename a column under the same condition. |
drop <table>: <comment> |
Generate a DROP TABLE for a table that exists but is no longer in the model. |
hint <table>: <regex> |
Select one of the alternative automatic migrations the table migrator commented out. |
depend <table>: t1, t2, … |
Delay the migration of <table> until the listed tables are migrated. |
-- |
A comment describing the hint; not copied into the generated SQL. |
Some rules worth knowing:
- Keywords are lowercase and case-sensitive. A line that happens to start with a keyword and contains
a colon (
after,drop, …) starts a new section, so write the SQL in uppercase. - Table and column names are converted to lowercase and may be schema-qualified
(
before md.customer:). The new name of arenameis given without schema, since a table can only be renamed within its schema. A trailing;after the new name is tolerated. - A
--comment needs a colon, just like any other keyword. A--line without a colon is not a keyword: before the first section it is ignored, but within a section it becomes part of the section's text. - Multiple sections for the same table and keyword are concatenated.
- If the
dependhints form a loop, the pending tables are migrated in arbitrary order, and aCAUTIONcomment and an error are reported.
Custom validators discovered via CustomMigrationValidator are run against each generated table
migration. A failed validation is reported as an error and fails the build.
Version filtering of hints¶
Hint files can be organized in version-named sub-directories so that only the hints newer than the
database's current version are applied. A backend's <minVersion> provides that current version and
may be:
- a fixed version string such as
3.1.0, - an SQL statement starting with
SELECT(case-insensitive), whose first column of the first row is the version, or - the fully-qualified class name of a
DatabaseVersionLoaderimplementation with a no-args constructor (which must be on the plugin classpath).
Without <minVersion>, there is no filtering: all hint files are applied.
The default SemanticVersionFileFilter interprets the name of the directory holding the hint file as a
semantic version (major.minor.hotfix, dots or underscores, optional -SNAPSHOT/-M1 suffix, which is
ignored) and applies the hint only if its version is strictly greater than the database version — a
database at 3.1.0 gets the hints in 3.1.1 and 3_2, but not those in 3.1.0. A version with more
components wins over its prefix, so 3.2.0 counts as newer than 3.2. Directories whose name does not
start with a digit are treated as permanent (always applied). An application can replace the filter
with its own @Service(VersionFileFilter.class) on the plugin classpath.
The validate Goal¶
A thin wrapper around migrate that forces validate=true: it generates the migration script as
usual but fails the build (database meta data differs from object model) as soon as any migration
would be necessary. Use it in CI to guarantee that a deployed database matches the committed model.
It accepts the same parameters as migrate.
The run Goal¶
Executes a single SQL script against every configured connectable backend, using the backend's script
runner. It inherits the same backend configuration as migrate, which is convenient because the
target database is usually the same. Name-only backends from backendNames are ignored, since they
cannot be connected to.
A development tool.
runis meant for the database on a developer's machine and for the test database of a build — trying out a generated migration, fixing up local data, looking at what is stored. It has no notion of which scripts a database has already seen, no versioning, no checksums and no rollback. Regular migrations belong to a migration tool such as Flyway or Liquibase, running the migration scripts thatmigrategenerated and that you committed to the repository. That is what makes a migration reproducible across staging and production, and auditable afterwards.
script(script, required): the SQL file to execute, read with the project's encoding (charset).
Every statement's outcome is logged: result sets as an aligned table with column headers, other
statements as their update count. The script is split on the backend's statement separator (;), and
-- line comments and /* … */ block comments are stripped before anything is sent to the database.
Two properties of the goal are easy to miss. It executes the script against every configured connectable backend, one after another — a project configuring PostgreSQL and H2 runs it twice. And it runs without a transaction: the connection is in autocommit mode, so a script that fails halfway leaves the statements before the failure committed and fails the build.
The document Goal¶
Renders the model as a set of static HTML or Markdown pages under docDir, nested one directory per
backend (docDir/<backend-name>/, same layout convention as create's sqlDir/<backend-name>/): one
page per entity plus an index.html (or index.md).
The entity pages¶
Each entity page starts with the entity's comment (the long comment, if there is one) and a meta table with the Table, Schema, Class ID and Extends rows (the latter two only where applicable).
The Table row shows the table name relative to its schema (the schema itself, if
any, is shown in the separate Schema row right below, so it isn't repeated). If mapSchemas is set,
the table name shown is the effective, flattened name actually used at runtime (see mapSchemas under
Model selection above) — and since a mapped entity no longer has a separate schema,
the Schema row is omitted for it.
Next come two attribute tables: Attributes, the entity's user-declared attributes, and
Framework Attributes, the attributes added implicitly by the framework — id/serial on every
entity, plus classId, tableSerial, rootId/rootClassId and the tokenlock/normText attributes
where the corresponding entity option is set (see
Root Columns and
Locking for what each one is for). Both
tables list each attribute's name, column, Java type, SQL type (e.g. VARCHAR(128), suffixed with
NOT NULL for non-nullable columns and the DEFAULT clause if the attribute declares one, exactly as
in the backend's DDL) and comment.
Below the attribute tables, an Indexes section lists the entity's additional, model-declared
indexes (an entity's ## indexes section — not the implicit primary key) as the actual,
backend-specific CREATE INDEX/CREATE UNIQUE INDEX statements, the same statements create would
emit for the same model.
Finally, the Relationships section shows the same information as the
ModelComment wurblet's
Javadoc comment block — referencing entities, composite/component structure, outgoing references, deep
references, composite membership and inheritance — except that every entity name mentioned is a
hyperlink to that entity's own page, so the whole model can be browsed by clicking through it instead of
manually finding the next file.
The index page¶
The index page has two sections: Aggregates lists each root entity that has components, with its
component tree (root entities without components are not aggregates in any useful sense and are left
out), and All Entities lists every entity with its comment. As in the ModelComment wurblet, an
N:M link component is followed by the entity on the opposite side, e.g.
User2Group [N:M] to UserGroup as userGroups. A component reachable from more than one aggregate is
expanded only under the first one.
Running it¶
mvn tentackle-sql:document -Dtentackle.backendNames=h2
mvn tentackle-sql:document -Dtentackle.backendNames=h2 -Dtentackle.docFormat=markdown
Never connects to a database — it only needs a Backend for its SQL-to-type mapping, not a connection —
but because that mapping can differ between backends, at least one must be selected, same as create
(see Backend selection above). Usually that is backendNames, but backends
configured via backendProperties or backends for the other goals are documented as well. One
self-contained page tree is generated per backend. There is no default — like create, nothing is
generated if no backend is configured. The model is taken from modelDir, or from the directories of
the filesets — each directory is loaded as a whole, the includes and excludes are ignored. The pages
are always written in UTF-8, and the dump parameters don't apply.
Parameters (beyond the shared ones listed above):
format(tentackle.docFormat): the output format,html(default) ormarkdown(aliasmd), case-insensitive. See Markdown output below.docDir(tentackle.docDir): directory to create the pages in, for both formats. Defaults to${project.build.directory}/model-doc.title(tentackle.htmlTitle): the heading shown on the index page, for both formats. Defaults to the Maven project's name.loadModelFromClassPath(tentackle.loadModelFromClassPath): also render entities from the model found on the classpath (e.g. a dependency's own model), so entities referenced from a dependency aren't dead links. Unlikemigrate,documentloads the local model files as well.cssFile(tentackle.cssFile): a project-specific stylesheet, copied verbatim tostyle.cssin every backend's page tree. HTML only — ignored (with a warning) formarkdown.
Markdown output¶
With format=markdown, the same content is written as <Entity>.md pages plus an index.md, for
dropping the model documentation into a Markdown-based doc tree (MkDocs, Bitbucket/GitHub, an IDE's
Markdown preview). Navigation is the same as for HTML: every entity mentioned — in the relationship
sections, the composite trees, the inheritance section and the Extends row — is a relative link to
that entity's .md page, every entity page starts with a link back to index.md, and the index links
every entity page.
The pages use GitHub-flavored Markdown: pipe tables for the meta and attribute tables, a fenced SQL code block for the indexes, and 4-space indentation for nested lists, which both CommonMark and Python-Markdown (MkDocs) render as nested. Model comments are escaped, so Markdown or HTML syntax in a comment shows up literally. No stylesheets are generated: the look is up to the Markdown renderer.
Styling the generated pages¶
Applies to the html format only. Each page loads two stylesheets: base.css, always generated, holds the structural rules (tables,
lists, spacing) and has no reason to be touched; style.css holds the theme (colors, fonts) and is
where a project's own look belongs. base.css's colors are var(--tentackle-doc-*, <neutral default>)
custom properties, so style.css only needs to redefine the ones it cares about — or ignore the
variables and override selectors outright, since style.css is loaded after base.css and wins the
cascade either way.
Without a cssFile, the generated style.css mimics tentackle.org's own brand: dark blue
headings and links, the signature orange-framed h2/h3 (a 1px top/bottom rule in the same color as
the heading text and link hover), a dark-blue-banded attribute table header, Roboto, and a
prefers-color-scheme: dark variant using the site's own dark-mode palette (lighter orange, muted
blue-gray links). It is the same palette applied to the docs site
(src/mkdocs/theme/stylesheets/extra.css) and the generated Maven site
(src/site/resources/css/site.css), so model documentation generated by this goal looks at home next
to the rest of the project's generated docs. The variables it sets —
--tentackle-doc-font, --tentackle-doc-text-color, --tentackle-doc-background-color,
--tentackle-doc-border-color, --tentackle-doc-heading-color, --tentackle-doc-accent-color,
--tentackle-doc-link-color, --tentackle-doc-comment-color,
--tentackle-doc-table-header-background-color, --tentackle-doc-table-header-text-color — are a
ready-made starting point for a cssFile: copy the generated style.css and change what you want,
including the @media (prefers-color-scheme: dark) block.
The reveng Goal¶
Every other goal on this page turns the Tentackle model into a database. reveng is the one goal
that goes the other way: it reverse-engineers tables of an existing database into
model source text, for introducing
Tentackle into a project whose schema already exists. It does not touch this plugin's own modelDir/
filesets or generate any Java code itself; it is a small, standalone first step that produces the
files tentackle-wizard:pdo -DbatchDir=… then consumes:
mvn tentackle-sql:reveng -Dtentackle.reveng.url=jdbc:postgresql://localhost/legacydb \
-Dtentackle.reveng.user=app -Dtentackle.reveng.password=secret \
-Dtentackle.reveng.tables=CUSTOMER,ORDER_ITEM
mvn tentackle-wizard:pdo -DbatchDir=target/reveng-model
For each requested table it reads columns, indexes and foreign keys via JDBC metadata
(org.tentackle.sql.metadata — the same introspection layer migrate/validate use to compare a
database against the model) and writes one model-source file into outputDir
(target/reveng-model by default). Parameters (all tentackle.reveng.* properties):
url,user,password(required exceptpassword): the JDBC connection to the existing database — independent of this plugin's ownbackends/backendPropertiesconfiguration, and typically only used once, for the reverse-engineering run itself.schemas: optional comma-separated list of schema names to restrict introspection to.tables(required): comma-separated list of table names to reverse-engineer.outputDir(default${project.build.directory}/reveng-model): where the generated model source files are written, one<EntityName>.mapper table.startClassId(default1): the class id assigned to the first table (in the order oftables); subsequent tables get consecutive ids. Batch-mode model source always needs a concrete, non-zero class id, so — exactly as when hand-writing batch model files — choosing a range that does not collide with the target project's already-assigned class ids is the caller's responsibility.integrity(defaultrelated): theintegrity :=value applied to every generated entity. The default assumes an application-checked relation with a database foreign key, since a physical foreign key was, by definition, already found in the database for any relation this goal emits.stereotype: optional stereotype (without the#) written into every generated entity, so thattentackle-wizard:pdo's batch mode can pick the matchingPdoProfilewhen more than one is configured.entityOptions: additional entity option flags written verbatim into every generated entity's global options line, comma-separated, e.g.remote, tracked.overwrite(defaultfalse): whether to overwrite files already present inoutputDir.
How names and types are derived. The table names in tables are matched case-insensitively.
Each table becomes an entity named in PascalCase after it (order_item → OrderItem). If two tables
map to the same entity name, the goal fails. Columns become camelCase attributes that keep the
original column name. The id and classid columns are skipped, since the model adds them
implicitly. A foreign-key column becomes an attribute named <relation>Id, where the relation name
is the column name without a trailing _id, or the target entity's name if those are the same. The
column type is mapped to one Java type per SQL type (e.g. VARCHAR(60) → String(60),
DECIMAL(10,2) → BigDecimal(10,2), CLOB → String(0), BLOB → Binary<byte[]>, an unknown type →
String). The files are written in UTF-8, and the log names the follow-up tentackle-wizard:pdo call.
What gets inferred, and what does not. A foreign key becomes a plain, non-composite object
relation using integrity; whether it should really be a component/composite relation, use
delete = cascade, or an eager select strategy cannot be inferred from schema metadata alone —
these are aggregate-design decisions the DDL does not encode, so the generated .map file is a
starting point to hand-edit before running pdo, not a finished model. A foreign key referencing a
table that is not part of the same reveng run is left as a plain attribute (with an explanatory
comment) instead of a relation, since pdo's batch mode loads every file in outputDir together and
cannot resolve a relation to an entity that isn't among them — reverse-engineer the referenced table
in the same run (or add the relation by hand afterward) if you need it. A table without a literal
id column does not fit Tentackle's synthetic-identity object model; it is still reverse-engineered,
but declared [nopkey] and will not behave like a normal identity-bearing PDO. Column NOT NULL
constraints are represented by choosing a Java primitive over its wrapper type where one exists (the
model has no other way to express column nullability); for types without a primitive form (String,
dates, BigDecimal, …) a NOT NULL column instead gets a @NotNull validation, since the database
constraint itself cannot be reproduced that way. Foreign keys spanning more than one column cannot be
expressed as a Tentackle relation and are left as plain attributes, and an index that duplicates
another one on the same columns (as some databases create for a foreign key) is skipped. Both are
logged as warnings.
The Skills¶
src/skills/ in this module holds three ready-made agent skills: tentackle-sql-migrator for
migrate/validate — generating the migration, what the migrator matches by itself, when a hint is
needed, and reading the script before it runs — tentackle-sql-runner for run, i.e. when to
prefer plain SQL over the domain-model tools and what to watch out for — and tentackle-sql-reveng
for reveng, i.e. what a foreign key becomes, what to check in the generated .map files before
handing them to tentackle-wizard:pdo, and the class-id range that goal knows nothing about. There is
deliberately none for create: it emits the DDL of the whole schema and belongs to bootstrapping an
empty database, not to changing an existing one. They are templates; copy them into your project:
See AI Agent Tooling Support for how it fits together with the model MCP server and the headless wizard goals.
How It Fits Into the Build¶
For a typical Tentackle application, the database tooling builds on the output of the wurbelizer:
- The wurbelizer extracts the entity model from the PDO interfaces and writes it to
target/wurbel/model. tentackle-sql:createturns that model into the initial schema script — run once to bootstrap a database, or wired into a profile that initializes the test database.- As the model evolves,
tentackle-sql:migrate(guided by migration hints) produces the incremental scripts that bring existing databases forward, andtentackle-sql:validateguards that databases stay in sync.
For a brownfield project — a database that exists before Tentackle is introduced — there is a step
0: tentackle-sql:reveng reads that existing database and writes the model source that
tentackle-wizard:pdo turns into the PDO interfaces the wurbelizer then extracts from in step 1.
Further Reading¶
- AI Agent Tooling Support — the
rungoal as one of the five tools a coding agent can drive, and when to prefer the others - Model definition — how entities and their relations are described
- Tentackle Maven Plugin — the companion plugin for service- and Java metadata generation
- Wurbelizer — the code generation engine that produces the model
- Plugin reference (generated)