Skip to content

feat(core): Aggregate all JPA entity/schema mismatches in one audit run#7

Merged
jeffjensen merged 1 commit into
mainfrom
feat/aggregate-jpa-schema-validation
Jul 1, 2026
Merged

feat(core): Aggregate all JPA entity/schema mismatches in one audit run#7
jeffjensen merged 1 commit into
mainfrom
feat/aggregate-jpa-schema-validation

Conversation

@jeffjensen

@jeffjensen jeffjensen commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

SchemaEntityValidationAudit previously only confirmed the EntityManagerFactory was built, leaning on Hibernate's fail-fast ddl-auto=validate startup check that aborts on the first mismatch. It now validates every mapped physical table and column against the live schema itself and returns one finding per missing table, missing column, and incompatible column type, reproducing Hibernate's own type-compatibility rule so a mapping Hibernate accepts is never flagged.

A ServiceLoader-registered MappingMetadataIntegrator captures each persistence unit's boot Metadata (keyed by SessionFactory UUID, so it still resolves through a proxied EntityManagerFactory such as Spring's); forEntityManagerFactory looks it back up at audit time.

Claude-Session: https://claude.ai/code/session_01UkRk16pW4VEM3E6vrDoG5y

Summary by CodeRabbit

  • New Features
    • Added a live schema-vs-mapping audit that checks for missing tables, missing columns, and incompatible column types.
    • Added boot-time captured metadata support and a new audit entry point that uses a DataSource.
    • Enhanced exclusions to support case-insensitive table/column names (including schema-qualified forms via table.column).
  • Bug Fixes
    • Improved failure behavior when mapping metadata isn’t available or the persistence provider isn’t Hibernate.
    • Captured metadata is cleared when the session factory is closed.
  • Documentation
    • Updated architecture and usage docs to reflect the new audit flow and ddl-auto=none guidance.
  • Tests
    • Added end-to-end integration tests against H2, plus new exception-focused unit tests.

@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 905972b5-86b4-409c-9c3f-8c01e6adf8da

📥 Commits

Reviewing files that changed from the base of the PR and between 0f893ab and aefb564.

📒 Files selected for processing (8)
  • src/main/java/io/github/databaseaudits/audit/jpa/MappingMetadataIntegrator.java
  • src/main/java/io/github/databaseaudits/audit/jpa/SchemaEntityValidationAudit.java
  • src/main/resources/META-INF/services/org.hibernate.integrator.spi.Integrator
  • src/site/asciidoc/architecture.adoc
  • src/site/asciidoc/audits.adoc
  • src/site/asciidoc/usage.adoc
  • src/test/java/io/github/databaseaudits/audit/jpa/SchemaEntityValidationAuditIT.java
  • src/test/java/io/github/databaseaudits/audit/jpa/SchemaEntityValidationAuditTest.java
✅ Files skipped from review due to trivial changes (2)
  • src/site/asciidoc/audits.adoc
  • src/site/asciidoc/architecture.adoc
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/main/resources/META-INF/services/org.hibernate.integrator.spi.Integrator
  • src/test/java/io/github/databaseaudits/audit/jpa/SchemaEntityValidationAuditTest.java
  • src/test/java/io/github/databaseaudits/audit/jpa/SchemaEntityValidationAuditIT.java
  • src/site/asciidoc/usage.adoc
  • src/main/java/io/github/databaseaudits/audit/jpa/MappingMetadataIntegrator.java
  • src/main/java/io/github/databaseaudits/audit/jpa/SchemaEntityValidationAudit.java

📝 Walkthrough

Walkthrough

This PR adds a Hibernate Integrator that captures boot-time Metadata per session factory. SchemaEntityValidationAudit is rewritten to compare mapped tables, columns, and types against live JDBC schema data, with docs and tests updated for the new bootstrap and audit flow.

Changes

Schema mapping audit

Layer / File(s) Summary
Boot metadata capture
src/main/java/io/github/databaseaudits/audit/jpa/MappingMetadataIntegrator.java, src/main/resources/META-INF/services/org.hibernate.integrator.spi.Integrator
Captures and removes Hibernate boot Metadata by session-factory UUID, exposes lookup by EntityManagerFactory, and registers the integrator as a service provider.
Live schema comparison
src/main/java/io/github/databaseaudits/audit/jpa/SchemaEntityValidationAudit.java
Stores a Metadata supplier and DataSource, builds from EntityManagerFactory, reads JDBC metadata, compares mapped tables/columns/types, applies exclusions, and reports schema mismatches.
Docs updates
src/site/asciidoc/architecture.adoc, src/site/asciidoc/audits.adoc, src/site/asciidoc/usage.adoc
Documents ddl-auto=none, the forEntityManagerFactory(emf, dataSource).audit() flow, and audit(Set<String>) exclusions.
Tests
src/test/java/io/github/databaseaudits/audit/jpa/SchemaEntityValidationAuditTest.java, src/test/java/io/github/databaseaudits/audit/jpa/SchemaEntityValidationAuditIT.java
Updates unit tests for exception cases and adds integration coverage for drift detection, exclusions, metadata capture, and teardown cleanup.

Estimated code review effort: 4 (Complex) | ~60 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 29.63% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: one audit run now reports all JPA entity/schema mismatches.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/aggregate-jpa-schema-validation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Aggregate JPA entity/schema validation mismatches in one audit run

✨ Enhancement 🧪 Tests 🕐 20-40 Minutes

Grey Divider

AI Description

• Capture Hibernate boot Metadata via Integrator for post-startup mapping inspection
• Validate all mapped tables/columns/types against live DatabaseMetaData and aggregate findings
• Add integration tests covering missing tables, missing columns, and type mismatches
Diagram

graph TD
HB["Hibernate bootstrap"] --> INT["MappingMetadataIntegrator"] --> STORE(("Metadata store"))
AUD["SchemaEntityValidationAudit"] --> STORE
AUD --> DS[/"DataSource"/] --> DB[("Live schema")]

subgraph Legend
  direction LR
  _svc["Module/Service"] ~~~ _cache(("In-memory store")) ~~~ _api[/"API"/] ~~~ _db[("Database")]
end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Invoke Hibernate SchemaValidator directly with custom handlers
  • ➕ Leverages Hibernate’s built-in schema validation path end-to-end
  • ➕ Potentially less duplicated type/normalization logic
  • ➖ Harder to make it aggregate findings (Hibernate validator is typically fail-fast)
  • ➖ Likely deeper coupling to internal Hibernate tooling and service registry wiring
2. Parse and diff INFORMATION_SCHEMA (DB-specific) instead of DatabaseMetaData
  • ➕ Richer and more consistent metadata for some databases (constraints, indexes, etc.)
  • ➕ Potentially faster for large schemas with targeted queries
  • ➖ Not portable across databases/vendors without per-dialect query sets
  • ➖ More maintenance burden than JDBC DatabaseMetaData

Recommendation: Current approach is the best fit for the stated goal: it stays database-portable via JDBC DatabaseMetaData, aggregates all mismatches in one run, and explicitly mirrors Hibernate’s own type-compatibility behavior to avoid false positives. The main tradeoff is some ongoing coupling to Hibernate APIs/semantics, which the integration tests help mitigate.

Files changed (5) +533 / -40

Enhancement (2) +329 / -27
MappingMetadataIntegrator.javaAdd Hibernate Integrator to capture boot Metadata by SessionFactory UUID +90/-0

Add Hibernate Integrator to capture boot Metadata by SessionFactory UUID

• Introduces a ServiceLoader-registered Hibernate Integrator that records each persistence unit’s boot-time Metadata in a concurrent map keyed by SessionFactory UUID. Removes recorded metadata on SessionFactory close and provides a static lookup that works through proxied EntityManagerFactory instances via unwrap().

src/main/java/io/github/databaseaudits/audit/jpa/MappingMetadataIntegrator.java

SchemaEntityValidationAudit.javaReplace fail-fast startup check with full schema introspection and aggregated findings +239/-27

Replace fail-fast startup check with full schema introspection and aggregated findings

• Refactors the audit to accept a Metadata supplier plus DataSource, then walks Hibernate’s resolved table/column mappings and compares them against live JDBC metadata. Reports one violation per missing table, missing column, or incompatible column type, reproducing Hibernate’s equivalentTypes/type-name matching logic and normalizations; throws if boot metadata is unavailable.

src/main/java/io/github/databaseaudits/audit/jpa/SchemaEntityValidationAudit.java

Tests (2) +203 / -13
SchemaEntityValidationAuditIT.javaAdd H2 end-to-end tests for aggregated table/column/type drift reporting +194/-0

Add H2 end-to-end tests for aggregated table/column/type drift reporting

• Adds integration tests that create real H2 schemas with intentional drift and assert the audit reports missing tables, missing columns, and type mismatches—including multiple issues in a single run. Also validates the ServiceLoader integrator captures metadata for a real SessionFactory and releases it on close.

src/test/java/io/github/databaseaudits/audit/jpa/SchemaEntityValidationAuditIT.java

SchemaEntityValidationAuditTest.javaUpdate unit test to expect failure when boot Metadata is not captured +9/-13

Update unit test to expect failure when boot Metadata is not captured

• Replaces the prior EntityManagerFactory presence checks with a focused assertion that the audit throws IllegalStateException when Metadata cannot be resolved (a cannot-run condition, not a violation).

src/test/java/io/github/databaseaudits/audit/jpa/SchemaEntityValidationAuditTest.java

Other (1) +1 / -0
org.hibernate.integrator.spi.IntegratorRegister MappingMetadataIntegrator via ServiceLoader +1/-0

Register MappingMetadataIntegrator via ServiceLoader

• Adds the standard Hibernate ServiceLoader entry so the integrator is automatically applied to all SessionFactories built with this module on the classpath.

src/main/resources/META-INF/services/org.hibernate.integrator.spi.Integrator

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (5) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 22 rules

Grey Divider


Action required

1. SchemaEntityValidationAudit missing exclusion set 📘 Rule violation ⚙ Maintainability
Description
SchemaEntityValidationAudit always reports every detected mismatch and provides no configurable
exclusion set to suppress known/accepted violations. This violates the audit-class requirement for
externally configurable suppression and limits safe adoption in real systems.
Code

src/main/java/io/github/databaseaudits/audit/jpa/SchemaEntityValidationAudit.java[R51-172]

public class SchemaEntityValidationAudit {
-    private final EntityManagerFactory entityManagerFactory;
+    private final Supplier<Metadata> metadataSupplier;
+    private final DataSource dataSource;
+
+    /**
+     * Constructs the audit around the boot mapping model and the datasource whose
+     * live schema it is checked against.
+     *
+     * @param metadataSupplier
+     *                             supplies the boot {@link Metadata} for the
+     *                             persistence unit, resolved when {@link #audit()}
+     *                             runs; see
+     *                             {@link #forEntityManagerFactory(EntityManagerFactory, DataSource)}.
+     * @param dataSource
+     *                             the datasource whose schema the mappings are
+     *                             validated against.
+     */
+    public SchemaEntityValidationAudit(final Supplier<Metadata> metadataSupplier,
+            final DataSource dataSource) {
+        this.metadataSupplier = metadataSupplier;
+        this.dataSource = dataSource;
+    }
+
+    /**
+     * Builds an audit for a JPA {@link EntityManagerFactory}, resolving its boot
+     * {@link Metadata} from {@link MappingMetadataIntegrator} when the audit runs.
+     *
+     * @param entityManagerFactory
+     *                                 the factory whose mappings to validate.
+     * @param dataSource
+     *                                 the datasource whose schema to validate
+     *                                 against.
+     * @return the audit.
+     */
+    public static SchemaEntityValidationAudit forEntityManagerFactory(
+            final EntityManagerFactory entityManagerFactory,
+            final DataSource dataSource) {
+        return new SchemaEntityValidationAudit(
+                () -> MappingMetadataIntegrator
+                        .metadataFor(entityManagerFactory),
+                dataSource);
+    }

    /**
-     * Returns a single violation when the {@link EntityManagerFactory} was not
-     * built (so startup validation never ran); an empty list when it was —
-     * which, under {@code ddl-auto=validate}, means the mappings matched the
-     * schema.
+     * Validates every mapped physical table against the live schema.
     *
-     * @return A single violation when the {@link EntityManagerFactory} is
-     *         {@code null}; otherwise an empty list.
+     * @return one violation per missing table, missing column, and incompatible
+     *         column type; an empty list when the mappings match the schema.
+     * @throws IllegalStateException
+     *                                   if the boot mapping model was never
+     *                                   captured (so the audit cannot run), or the
+     *                                   database metadata cannot be read.
     */
    public List<String> audit() {
-        if (entityManagerFactory == null) {
-            return List.of(
-                    "EntityManagerFactory should have been built under ddl-auto=validate.");
+        final Metadata metadata = metadataSupplier.get();
+        if (metadata == null) {
+            throw new IllegalStateException(
+                    "JPA mapping metadata was not captured for this EntityManagerFactory; ensure "
+                            + "database-audits-core is on the test classpath (it registers a Hibernate "
+                            + "Integrator that records the boot metadata) and that the EntityManagerFactory "
+                            + "has been built.");
+        }
+        final Database database = metadata.getDatabase();
+        final Dialect dialect = database.getDialect();
+        final JdbcTypeRegistry jdbcTypeRegistry =
+                database.getTypeConfiguration().getJdbcTypeRegistry();
+
+        final List<String> violations = new ArrayList<>();
+        try (Connection connection = dataSource.getConnection()) {
+            final DatabaseMetaData databaseMetaData = connection.getMetaData();
+            final String catalog = connection.getCatalog();
+            final String defaultSchema = connection.getSchema();
+            final Map<String, Map<String, Map<String, DatabaseColumn>>> columnsBySchema =
+                    new HashMap<>();
+
+            for (final Table table : metadata.collectTableMappings()) {
+                if (!table.isPhysicalTable()) {
+                    continue;
+                }
+                final String schema =
+                        table.getSchema() != null ? table.getSchema()
+                                : defaultSchema;
+                final Map<String, DatabaseColumn> databaseColumns =
+                        columnsBySchema
+                                .computeIfAbsent(schema,
+                                        s -> readColumns(databaseMetaData,
+                                                catalog, s))
+                                .get(canonical(table.getName()));
+                if (databaseColumns == null) {
+                    violations.add("missing table ["
+                            + qualifiedName(schema, table.getName()) + "]");
+                    continue;
+                }
+                for (final Column column : table.getColumns()) {
+                    final DatabaseColumn databaseColumn =
+                            databaseColumns.get(canonical(column.getName()));
+                    if (databaseColumn == null) {
+                        violations.add("missing column [" + column.getName()
+                                + "] in table ["
+                                + qualifiedName(schema, table.getName()) + "]");
+                    } else if (!hasMatchingType(column, databaseColumn, metadata,
+                            dialect, jdbcTypeRegistry)) {
+                        violations.add("wrong column type in column ["
+                                + column.getName() + "] in table ["
+                                + qualifiedName(schema, table.getName())
+                                + "]; found ["
+                                + databaseColumn.typeName()
+                                        .toLowerCase(Locale.ROOT)
+                                + "], but expecting ["
+                                + column.getSqlType(metadata)
+                                        .toLowerCase(Locale.ROOT)
+                                + "]");
+                    }
+                }
+            }
+        } catch (final SQLException e) {
+            throw new IllegalStateException(
+                    "Failed to read database metadata for JPA entity/schema validation.",
+                    e);
+        }
+        return violations;
+    }
Evidence
PR Compliance ID 1492984 requires audit classes to accept and apply configurable exclusion sets. The
added/modified audit class has only metadataSupplier and dataSource fields and unconditionally
adds violations during schema/table/column iteration, with no exclusion mechanism.

Rule 1492984: Audit classes must support configurable exclusion sets for suppressing known violations
src/main/java/io/github/databaseaudits/audit/jpa/SchemaEntityValidationAudit.java[51-172]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`SchemaEntityValidationAudit` reports all detected table/column/type mismatches unconditionally, with no way to configure exclusions for known/acceptable drift. Compliance requires audit classes to support configurable exclusion sets and apply them before reporting.

## Issue Context
The audit currently only accepts `Supplier<Metadata>` and `DataSource` and then unconditionally adds strings to `violations` while iterating tables/columns.

## Fix Focus Areas
- src/main/java/io/github/databaseaudits/audit/jpa/SchemaEntityValidationAudit.java[51-172]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. metadataFor() missing @nullable 📘 Rule violation ≡ Correctness
Description
MappingMetadataIntegrator.metadataFor(...) is documented to return null when no metadata was
recorded, but the return type is not annotated with JSpecify @Nullable. This creates an incorrect
nullability contract for callers and static analysis.
Code

src/main/java/io/github/databaseaudits/audit/jpa/MappingMetadataIntegrator.java[R75-89]

+    /**
+     * Returns the boot {@link Metadata} captured for the given factory.
+     *
+     * @param entityManagerFactory
+     *                                 the factory whose mapping model is wanted;
+     *                                 may be a proxy over the native factory.
+     * @return the captured {@link Metadata}, or {@code null} if none was recorded
+     *         — for instance when the factory was built without this module on
+     *         its classpath, or after the factory has been closed.
+     */
+    public static Metadata metadataFor(
+            final EntityManagerFactory entityManagerFactory) {
+        return METADATA_BY_UUID.get(entityManagerFactory
+                .unwrap(SessionFactoryImplementor.class).getUuid());
+    }
Evidence
PR Compliance ID 1492988 requires nullable Java references to be annotated with JSpecify @Nullable
when null is allowed. The Javadoc states the method may return null, but the signature is `public
static Metadata metadataFor(...) with no @Nullable`.

Rule 1492988: Annotate nullable Java references with JSpecify @Nullable
src/main/java/io/github/databaseaudits/audit/jpa/MappingMetadataIntegrator.java[75-89]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`MappingMetadataIntegrator.metadataFor(...)` can return `null` (per its Javadoc) but the method signature is not annotated with `@org.jspecify.annotations.Nullable`.

## Issue Context
The repository uses JSpecify, and this method explicitly documents `null` as a valid return value.

## Fix Focus Areas
- src/main/java/io/github/databaseaudits/audit/jpa/MappingMetadataIntegrator.java[75-89]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Helper methods return null unannotated 📘 Rule violation ≡ Correctness
Description
Several new helper methods explicitly accept/return null (via if (x == null) return null /
ternary null returns) but do not annotate those parameters/returns with JSpecify @Nullable. This
violates the project's nullability annotation requirement and weakens static checking.
Code

src/main/java/io/github/databaseaudits/audit/jpa/SchemaEntityValidationAudit.java[R224-257]

+    private static String normalize(final String typeName) {
+        if (typeName == null) {
+            return null;
+        }
+        final String lowercase = typeName.toLowerCase(Locale.ROOT);
+        return switch (lowercase) {
+            case "int" -> "integer";
+            case "character" -> "char";
+            case "character varying" -> "varchar";
+            case "binary varying" -> "varbinary";
+            case "character large object" -> "clob";
+            case "binary large object" -> "blob";
+            case "interval second" -> "interval";
+            case "double precision" -> "double";
+            default -> lowercase;
+        };
+    }
+
+    private static String stripArguments(final String typeExpression) {
+        if (typeExpression == null) {
+            return null;
+        }
+        final int parenthesis = typeExpression.indexOf('(');
+        return parenthesis > 0 ? typeExpression.substring(0, parenthesis).trim()
+                : typeExpression;
+    }
+
+    private static String qualifiedName(final String schema, final String name) {
+        return schema == null ? name : schema + "." + name;
+    }
+
+    private static String canonical(final String identifier) {
+        return identifier == null ? null : identifier.toLowerCase(Locale.ROOT);
+    }
Evidence
PR Compliance ID 1492988 requires annotating reference-typed declarations when null is allowed. In
the shown methods, null checks/returns indicate null is permitted, but the method signatures remain
unannotated.

Rule 1492988: Annotate nullable Java references with JSpecify @Nullable
src/main/java/io/github/databaseaudits/audit/jpa/SchemaEntityValidationAudit.java[224-257]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
New helpers in `SchemaEntityValidationAudit` (`normalize`, `stripArguments`, `canonical`) explicitly treat inputs/outputs as nullable but lack `@org.jspecify.annotations.Nullable` on those parameters/return types.

## Issue Context
The code contains explicit null checks and returns `null` in these methods, which is evidence that null is part of the contract.

## Fix Focus Areas
- src/main/java/io/github/databaseaudits/audit/jpa/SchemaEntityValidationAudit.java[224-257]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (2)
4. assertThat(violation) missing .as() 📘 Rule violation ⚙ Maintainability
Description
Multiple AssertJ assertion chains inside anySatisfy(...) omit .as("...".) descriptions. This
violates the requirement that each AssertJ assertion include a description ending with a period.
Code

src/test/java/io/github/databaseaudits/audit/jpa/SchemaEntityValidationAuditIT.java[R48-93]

+    void testAudit_MissingTable_ReportsMissingTable() throws SQLException {
+        assertThat(auditAgainst(CLEAN_PARENT))
+                .as("Entity with no table should be reported as a missing table.")
+                .anySatisfy(violation -> assertThat(violation)
+                        .contains("missing table").contains("child"));
+    }
+
+    @Test
+    void testAudit_MissingColumn_ReportsMissingColumn() throws SQLException {
+        assertThat(auditAgainst(
+                "CREATE TABLE parent (id BIGINT PRIMARY KEY, name VARCHAR(255))",
+                CLEAN_CHILD))
+                .as("Mapped column absent from the table should be reported.")
+                .anySatisfy(violation -> assertThat(violation)
+                        .contains("missing column").contains("quantity")
+                        .contains("parent"));
+    }
+
+    @Test
+    void testAudit_WrongColumnType_ReportsTypeMismatch() throws SQLException {
+        assertThat(auditAgainst(
+                "CREATE TABLE parent (id BIGINT PRIMARY KEY, name INTEGER, quantity INTEGER)",
+                CLEAN_CHILD))
+                .as("Column whose type differs from the mapping should be reported.")
+                .anySatisfy(violation -> assertThat(violation)
+                        .contains("wrong column type").contains("name")
+                        .contains("varchar"));
+    }
+
+    @Test
+    void testAudit_MultipleDriftsInOneSchema_ReportsThemAllAtOnce()
+            throws SQLException {
+        final List<String> violations = auditAgainst(
+                "CREATE TABLE parent (id BIGINT PRIMARY KEY, name INTEGER)");
+
+        assertThat(violations)
+                .as("Every mismatch should surface in a single run: child's "
+                        + "missing table, parent's missing quantity column, and "
+                        + "parent.name's wrong type.")
+                .hasSize(3)
+                .anySatisfy(violation -> assertThat(violation)
+                        .contains("missing table").contains("child"))
+                .anySatisfy(violation -> assertThat(violation)
+                        .contains("missing column").contains("quantity"))
+                .anySatisfy(violation -> assertThat(violation)
+                        .contains("wrong column type").contains("name"));
Evidence
PR Compliance ID 1493065 requires every AssertJ assertion chain to include .as("...".). The outer
assertions include .as(...), but the nested assertThat(violation)...contains(...) chains do not.

Rule 1493065: AssertJ assertions must include a .as() description ending with a period
src/test/java/io/github/databaseaudits/audit/jpa/SchemaEntityValidationAuditIT.java[48-93]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
AssertJ assertions inside the `anySatisfy(...)` lambdas do not include `.as("...".)` descriptions.

## Issue Context
The rule requires a non-empty string literal description ending with `.` for each AssertJ assertion chain.

## Fix Focus Areas
- src/test/java/io/github/databaseaudits/audit/jpa/SchemaEntityValidationAuditIT.java[48-93]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Unwrap failures unhandled 🐞 Bug ☼ Reliability
Description
MappingMetadataIntegrator.metadataFor unconditionally calls
entityManagerFactory.unwrap(SessionFactoryImplementor.class); if unwrap is unsupported or fails,
the audit throws an unrelated runtime exception instead of the intended “metadata was not captured”
IllegalStateException.
Code

src/main/java/io/github/databaseaudits/audit/jpa/MappingMetadataIntegrator.java[R85-89]

+    public static Metadata metadataFor(
+            final EntityManagerFactory entityManagerFactory) {
+        return METADATA_BY_UUID.get(entityManagerFactory
+                .unwrap(SessionFactoryImplementor.class).getUuid());
+    }
Evidence
The metadata lookup path calls unwrap(SessionFactoryImplementor.class) without any guarding;
SchemaEntityValidationAudit only handles the metadata == null case, so exceptions thrown during
unwrap bypass the intended error message entirely.

src/main/java/io/github/databaseaudits/audit/jpa/MappingMetadataIntegrator.java[85-89]
src/main/java/io/github/databaseaudits/audit/jpa/SchemaEntityValidationAudit.java[104-112]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`MappingMetadataIntegrator.metadataFor(...)` can throw during `unwrap(SessionFactoryImplementor.class)` before SchemaEntityValidationAudit can handle the “metadata missing” case, producing confusing failures.

### Issue Context
SchemaEntityValidationAudit.audit() expects `metadataSupplier.get()` to either return Metadata or `null` so it can throw a clear `IllegalStateException` explaining how to enable metadata capture.

### Fix Focus Areas
- src/main/java/io/github/databaseaudits/audit/jpa/MappingMetadataIntegrator.java[85-89]
- src/main/java/io/github/databaseaudits/audit/jpa/SchemaEntityValidationAudit.java[104-112]

### Suggested changes
- In `MappingMetadataIntegrator.metadataFor(...)`:
 - Validate `entityManagerFactory != null`.
 - Wrap `unwrap(...)` in a try/catch (e.g., `RuntimeException`) and either:
   - return `null` (so the audit emits its existing “metadata not captured” message), or
   - throw a new `IllegalStateException` with a targeted message (“EntityManagerFactory is not a Hibernate SessionFactory; cannot resolve UUID for metadata lookup”) and include the caught exception as the cause.
- Optionally, in `SchemaEntityValidationAudit.audit()` catch exceptions thrown by `metadataSupplier.get()` and rethrow as an `IllegalStateException` that preserves the current actionable guidance plus the original cause.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

6. Complex computeIfAbsent().get() chain 📘 Rule violation ⚙ Maintainability
Description
The statement computing databaseColumns chains multiple operations (computeIfAbsent(...) +
lambda + .get(...)) in a single expression. This exceeds the maximum expression complexity per
statement and reduces readability/maintainability.
Code

src/main/java/io/github/databaseaudits/audit/jpa/SchemaEntityValidationAudit.java[R133-138]

+                final Map<String, DatabaseColumn> databaseColumns =
+                        columnsBySchema
+                                .computeIfAbsent(schema,
+                                        s -> readColumns(databaseMetaData,
+                                                catalog, s))
+                                .get(canonical(table.getName()));
Evidence
PR Compliance ID 1493000 limits statement expression complexity by requiring decomposition into
named intermediate variables when more than two operations are combined. The cited line chains
multiple operations into one statement.

Rule 1493000: Limit expression complexity per statement
src/main/java/io/github/databaseaudits/audit/jpa/SchemaEntityValidationAudit.java[133-138]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A multi-step map lookup/build is implemented as a single chained expression, increasing per-statement expression complexity.

## Issue Context
The code both creates/fetches the schema map and then fetches the table entry in the same statement.

## Fix Focus Areas
- src/main/java/io/github/databaseaudits/audit/jpa/SchemaEntityValidationAudit.java[133-138]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Docs now incorrect 🐞 Bug ⚙ Maintainability
Description
The site docs/examples still use the removed new SchemaEntityValidationAudit(entityManagerFactory)
constructor and describe relying on ddl-auto=validate, but the class now requires
Supplier<Metadata> + DataSource and explicitly expects ddl-auto=none, so users following the
docs will not compile or will run the audit with the wrong configuration.
Code

src/main/java/io/github/databaseaudits/audit/jpa/SchemaEntityValidationAudit.java[R51-92]

public class SchemaEntityValidationAudit {
-    private final EntityManagerFactory entityManagerFactory;
+    private final Supplier<Metadata> metadataSupplier;
+    private final DataSource dataSource;
+
+    /**
+     * Constructs the audit around the boot mapping model and the datasource whose
+     * live schema it is checked against.
+     *
+     * @param metadataSupplier
+     *                             supplies the boot {@link Metadata} for the
+     *                             persistence unit, resolved when {@link #audit()}
+     *                             runs; see
+     *                             {@link #forEntityManagerFactory(EntityManagerFactory, DataSource)}.
+     * @param dataSource
+     *                             the datasource whose schema the mappings are
+     *                             validated against.
+     */
+    public SchemaEntityValidationAudit(final Supplier<Metadata> metadataSupplier,
+            final DataSource dataSource) {
+        this.metadataSupplier = metadataSupplier;
+        this.dataSource = dataSource;
+    }
+
+    /**
+     * Builds an audit for a JPA {@link EntityManagerFactory}, resolving its boot
+     * {@link Metadata} from {@link MappingMetadataIntegrator} when the audit runs.
+     *
+     * @param entityManagerFactory
+     *                                 the factory whose mappings to validate.
+     * @param dataSource
+     *                                 the datasource whose schema to validate
+     *                                 against.
+     * @return the audit.
+     */
+    public static SchemaEntityValidationAudit forEntityManagerFactory(
+            final EntityManagerFactory entityManagerFactory,
+            final DataSource dataSource) {
+        return new SchemaEntityValidationAudit(
+                () -> MappingMetadataIntegrator
+                        .metadataFor(entityManagerFactory),
+                dataSource);
+    }
Evidence
Docs still reference the old constructor and ddl-auto=validate behavior, while the class now
exposes only a (Supplier<Metadata>, DataSource) constructor plus forEntityManagerFactory(...)
and explicitly documents running with ddl-auto=none.

src/site/asciidoc/audits.adoc[235-251]
src/site/asciidoc/usage.adoc[91-105]
src/main/java/io/github/databaseaudits/audit/jpa/SchemaEntityValidationAudit.java[51-92]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The published Asciidoc documentation is stale after the SchemaEntityValidationAudit API/behavior change. It still shows the old constructor and `ddl-auto=validate` guidance, which no longer matches the implementation.

### Issue Context
The PR changes SchemaEntityValidationAudit to require a `DataSource` and a boot `Metadata` supplier (via `forEntityManagerFactory` / MappingMetadataIntegrator), and recommends running with `ddl-auto=none`.

### Fix Focus Areas
- src/site/asciidoc/audits.adoc[235-251]
- src/site/asciidoc/usage.adoc[91-105]
- src/main/java/io/github/databaseaudits/audit/jpa/SchemaEntityValidationAudit.java[51-92]

### Suggested changes
- Replace `new SchemaEntityValidationAudit(entityManagerFactory)` examples with `SchemaEntityValidationAudit.forEntityManagerFactory(emf, dataSource)` (or show constructing with `(Supplier<Metadata>, DataSource)` where appropriate).
- Update narrative to reflect that this audit does **not** rely on `ddl-auto=validate` and should be run with `ddl-auto=none` (per the new class Javadoc).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment on lines 51 to +172
public class SchemaEntityValidationAudit {
private final EntityManagerFactory entityManagerFactory;
private final Supplier<Metadata> metadataSupplier;
private final DataSource dataSource;

/**
* Constructs the audit around the boot mapping model and the datasource whose
* live schema it is checked against.
*
* @param metadataSupplier
* supplies the boot {@link Metadata} for the
* persistence unit, resolved when {@link #audit()}
* runs; see
* {@link #forEntityManagerFactory(EntityManagerFactory, DataSource)}.
* @param dataSource
* the datasource whose schema the mappings are
* validated against.
*/
public SchemaEntityValidationAudit(final Supplier<Metadata> metadataSupplier,
final DataSource dataSource) {
this.metadataSupplier = metadataSupplier;
this.dataSource = dataSource;
}

/**
* Builds an audit for a JPA {@link EntityManagerFactory}, resolving its boot
* {@link Metadata} from {@link MappingMetadataIntegrator} when the audit runs.
*
* @param entityManagerFactory
* the factory whose mappings to validate.
* @param dataSource
* the datasource whose schema to validate
* against.
* @return the audit.
*/
public static SchemaEntityValidationAudit forEntityManagerFactory(
final EntityManagerFactory entityManagerFactory,
final DataSource dataSource) {
return new SchemaEntityValidationAudit(
() -> MappingMetadataIntegrator
.metadataFor(entityManagerFactory),
dataSource);
}

/**
* Returns a single violation when the {@link EntityManagerFactory} was not
* built (so startup validation never ran); an empty list when it was —
* which, under {@code ddl-auto=validate}, means the mappings matched the
* schema.
* Validates every mapped physical table against the live schema.
*
* @return A single violation when the {@link EntityManagerFactory} is
* {@code null}; otherwise an empty list.
* @return one violation per missing table, missing column, and incompatible
* column type; an empty list when the mappings match the schema.
* @throws IllegalStateException
* if the boot mapping model was never
* captured (so the audit cannot run), or the
* database metadata cannot be read.
*/
public List<String> audit() {
if (entityManagerFactory == null) {
return List.of(
"EntityManagerFactory should have been built under ddl-auto=validate.");
final Metadata metadata = metadataSupplier.get();
if (metadata == null) {
throw new IllegalStateException(
"JPA mapping metadata was not captured for this EntityManagerFactory; ensure "
+ "database-audits-core is on the test classpath (it registers a Hibernate "
+ "Integrator that records the boot metadata) and that the EntityManagerFactory "
+ "has been built.");
}
final Database database = metadata.getDatabase();
final Dialect dialect = database.getDialect();
final JdbcTypeRegistry jdbcTypeRegistry =
database.getTypeConfiguration().getJdbcTypeRegistry();

final List<String> violations = new ArrayList<>();
try (Connection connection = dataSource.getConnection()) {
final DatabaseMetaData databaseMetaData = connection.getMetaData();
final String catalog = connection.getCatalog();
final String defaultSchema = connection.getSchema();
final Map<String, Map<String, Map<String, DatabaseColumn>>> columnsBySchema =
new HashMap<>();

for (final Table table : metadata.collectTableMappings()) {
if (!table.isPhysicalTable()) {
continue;
}
final String schema =
table.getSchema() != null ? table.getSchema()
: defaultSchema;
final Map<String, DatabaseColumn> databaseColumns =
columnsBySchema
.computeIfAbsent(schema,
s -> readColumns(databaseMetaData,
catalog, s))
.get(canonical(table.getName()));
if (databaseColumns == null) {
violations.add("missing table ["
+ qualifiedName(schema, table.getName()) + "]");
continue;
}
for (final Column column : table.getColumns()) {
final DatabaseColumn databaseColumn =
databaseColumns.get(canonical(column.getName()));
if (databaseColumn == null) {
violations.add("missing column [" + column.getName()
+ "] in table ["
+ qualifiedName(schema, table.getName()) + "]");
} else if (!hasMatchingType(column, databaseColumn, metadata,
dialect, jdbcTypeRegistry)) {
violations.add("wrong column type in column ["
+ column.getName() + "] in table ["
+ qualifiedName(schema, table.getName())
+ "]; found ["
+ databaseColumn.typeName()
.toLowerCase(Locale.ROOT)
+ "], but expecting ["
+ column.getSqlType(metadata)
.toLowerCase(Locale.ROOT)
+ "]");
}
}
}
} catch (final SQLException e) {
throw new IllegalStateException(
"Failed to read database metadata for JPA entity/schema validation.",
e);
}
return violations;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. schemaentityvalidationaudit missing exclusion set 📘 Rule violation ⚙ Maintainability

SchemaEntityValidationAudit always reports every detected mismatch and provides no configurable
exclusion set to suppress known/accepted violations. This violates the audit-class requirement for
externally configurable suppression and limits safe adoption in real systems.
Agent Prompt
## Issue description
`SchemaEntityValidationAudit` reports all detected table/column/type mismatches unconditionally, with no way to configure exclusions for known/acceptable drift. Compliance requires audit classes to support configurable exclusion sets and apply them before reporting.

## Issue Context
The audit currently only accepts `Supplier<Metadata>` and `DataSource` and then unconditionally adds strings to `violations` while iterating tables/columns.

## Fix Focus Areas
- src/main/java/io/github/databaseaudits/audit/jpa/SchemaEntityValidationAudit.java[51-172]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +75 to +89
/**
* Returns the boot {@link Metadata} captured for the given factory.
*
* @param entityManagerFactory
* the factory whose mapping model is wanted;
* may be a proxy over the native factory.
* @return the captured {@link Metadata}, or {@code null} if none was recorded
* — for instance when the factory was built without this module on
* its classpath, or after the factory has been closed.
*/
public static Metadata metadataFor(
final EntityManagerFactory entityManagerFactory) {
return METADATA_BY_UUID.get(entityManagerFactory
.unwrap(SessionFactoryImplementor.class).getUuid());
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. metadatafor() missing @nullable 📘 Rule violation ≡ Correctness

MappingMetadataIntegrator.metadataFor(...) is documented to return null when no metadata was
recorded, but the return type is not annotated with JSpecify @Nullable. This creates an incorrect
nullability contract for callers and static analysis.
Agent Prompt
## Issue description
`MappingMetadataIntegrator.metadataFor(...)` can return `null` (per its Javadoc) but the method signature is not annotated with `@org.jspecify.annotations.Nullable`.

## Issue Context
The repository uses JSpecify, and this method explicitly documents `null` as a valid return value.

## Fix Focus Areas
- src/main/java/io/github/databaseaudits/audit/jpa/MappingMetadataIntegrator.java[75-89]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +224 to +257
private static String normalize(final String typeName) {
if (typeName == null) {
return null;
}
final String lowercase = typeName.toLowerCase(Locale.ROOT);
return switch (lowercase) {
case "int" -> "integer";
case "character" -> "char";
case "character varying" -> "varchar";
case "binary varying" -> "varbinary";
case "character large object" -> "clob";
case "binary large object" -> "blob";
case "interval second" -> "interval";
case "double precision" -> "double";
default -> lowercase;
};
}

private static String stripArguments(final String typeExpression) {
if (typeExpression == null) {
return null;
}
final int parenthesis = typeExpression.indexOf('(');
return parenthesis > 0 ? typeExpression.substring(0, parenthesis).trim()
: typeExpression;
}

private static String qualifiedName(final String schema, final String name) {
return schema == null ? name : schema + "." + name;
}

private static String canonical(final String identifier) {
return identifier == null ? null : identifier.toLowerCase(Locale.ROOT);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

3. Helper methods return null unannotated 📘 Rule violation ≡ Correctness

Several new helper methods explicitly accept/return null (via if (x == null) return null /
ternary null returns) but do not annotate those parameters/returns with JSpecify @Nullable. This
violates the project's nullability annotation requirement and weakens static checking.
Agent Prompt
## Issue description
New helpers in `SchemaEntityValidationAudit` (`normalize`, `stripArguments`, `canonical`) explicitly treat inputs/outputs as nullable but lack `@org.jspecify.annotations.Nullable` on those parameters/return types.

## Issue Context
The code contains explicit null checks and returns `null` in these methods, which is evidence that null is part of the contract.

## Fix Focus Areas
- src/main/java/io/github/databaseaudits/audit/jpa/SchemaEntityValidationAudit.java[224-257]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +48 to +93
void testAudit_MissingTable_ReportsMissingTable() throws SQLException {
assertThat(auditAgainst(CLEAN_PARENT))
.as("Entity with no table should be reported as a missing table.")
.anySatisfy(violation -> assertThat(violation)
.contains("missing table").contains("child"));
}

@Test
void testAudit_MissingColumn_ReportsMissingColumn() throws SQLException {
assertThat(auditAgainst(
"CREATE TABLE parent (id BIGINT PRIMARY KEY, name VARCHAR(255))",
CLEAN_CHILD))
.as("Mapped column absent from the table should be reported.")
.anySatisfy(violation -> assertThat(violation)
.contains("missing column").contains("quantity")
.contains("parent"));
}

@Test
void testAudit_WrongColumnType_ReportsTypeMismatch() throws SQLException {
assertThat(auditAgainst(
"CREATE TABLE parent (id BIGINT PRIMARY KEY, name INTEGER, quantity INTEGER)",
CLEAN_CHILD))
.as("Column whose type differs from the mapping should be reported.")
.anySatisfy(violation -> assertThat(violation)
.contains("wrong column type").contains("name")
.contains("varchar"));
}

@Test
void testAudit_MultipleDriftsInOneSchema_ReportsThemAllAtOnce()
throws SQLException {
final List<String> violations = auditAgainst(
"CREATE TABLE parent (id BIGINT PRIMARY KEY, name INTEGER)");

assertThat(violations)
.as("Every mismatch should surface in a single run: child's "
+ "missing table, parent's missing quantity column, and "
+ "parent.name's wrong type.")
.hasSize(3)
.anySatisfy(violation -> assertThat(violation)
.contains("missing table").contains("child"))
.anySatisfy(violation -> assertThat(violation)
.contains("missing column").contains("quantity"))
.anySatisfy(violation -> assertThat(violation)
.contains("wrong column type").contains("name"));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

4. assertthat(violation) missing .as() 📘 Rule violation ⚙ Maintainability

Multiple AssertJ assertion chains inside anySatisfy(...) omit .as("...".) descriptions. This
violates the requirement that each AssertJ assertion include a description ending with a period.
Agent Prompt
## Issue description
AssertJ assertions inside the `anySatisfy(...)` lambdas do not include `.as("...".)` descriptions.

## Issue Context
The rule requires a non-empty string literal description ending with `.` for each AssertJ assertion chain.

## Fix Focus Areas
- src/test/java/io/github/databaseaudits/audit/jpa/SchemaEntityValidationAuditIT.java[48-93]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +133 to +138
final Map<String, DatabaseColumn> databaseColumns =
columnsBySchema
.computeIfAbsent(schema,
s -> readColumns(databaseMetaData,
catalog, s))
.get(canonical(table.getName()));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

5. Complex computeifabsent().get() chain 📘 Rule violation ⚙ Maintainability

The statement computing databaseColumns chains multiple operations (computeIfAbsent(...) +
lambda + .get(...)) in a single expression. This exceeds the maximum expression complexity per
statement and reduces readability/maintainability.
Agent Prompt
## Issue description
A multi-step map lookup/build is implemented as a single chained expression, increasing per-statement expression complexity.

## Issue Context
The code both creates/fetches the schema map and then fetches the table entry in the same statement.

## Fix Focus Areas
- src/main/java/io/github/databaseaudits/audit/jpa/SchemaEntityValidationAudit.java[133-138]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines 51 to +92
public class SchemaEntityValidationAudit {
private final EntityManagerFactory entityManagerFactory;
private final Supplier<Metadata> metadataSupplier;
private final DataSource dataSource;

/**
* Constructs the audit around the boot mapping model and the datasource whose
* live schema it is checked against.
*
* @param metadataSupplier
* supplies the boot {@link Metadata} for the
* persistence unit, resolved when {@link #audit()}
* runs; see
* {@link #forEntityManagerFactory(EntityManagerFactory, DataSource)}.
* @param dataSource
* the datasource whose schema the mappings are
* validated against.
*/
public SchemaEntityValidationAudit(final Supplier<Metadata> metadataSupplier,
final DataSource dataSource) {
this.metadataSupplier = metadataSupplier;
this.dataSource = dataSource;
}

/**
* Builds an audit for a JPA {@link EntityManagerFactory}, resolving its boot
* {@link Metadata} from {@link MappingMetadataIntegrator} when the audit runs.
*
* @param entityManagerFactory
* the factory whose mappings to validate.
* @param dataSource
* the datasource whose schema to validate
* against.
* @return the audit.
*/
public static SchemaEntityValidationAudit forEntityManagerFactory(
final EntityManagerFactory entityManagerFactory,
final DataSource dataSource) {
return new SchemaEntityValidationAudit(
() -> MappingMetadataIntegrator
.metadataFor(entityManagerFactory),
dataSource);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

6. Docs now incorrect 🐞 Bug ⚙ Maintainability

The site docs/examples still use the removed new SchemaEntityValidationAudit(entityManagerFactory)
constructor and describe relying on ddl-auto=validate, but the class now requires
Supplier<Metadata> + DataSource and explicitly expects ddl-auto=none, so users following the
docs will not compile or will run the audit with the wrong configuration.
Agent Prompt
### Issue description
The published Asciidoc documentation is stale after the SchemaEntityValidationAudit API/behavior change. It still shows the old constructor and `ddl-auto=validate` guidance, which no longer matches the implementation.

### Issue Context
The PR changes SchemaEntityValidationAudit to require a `DataSource` and a boot `Metadata` supplier (via `forEntityManagerFactory` / MappingMetadataIntegrator), and recommends running with `ddl-auto=none`.

### Fix Focus Areas
- src/site/asciidoc/audits.adoc[235-251]
- src/site/asciidoc/usage.adoc[91-105]
- src/main/java/io/github/databaseaudits/audit/jpa/SchemaEntityValidationAudit.java[51-92]

### Suggested changes
- Replace `new SchemaEntityValidationAudit(entityManagerFactory)` examples with `SchemaEntityValidationAudit.forEntityManagerFactory(emf, dataSource)` (or show constructing with `(Supplier<Metadata>, DataSource)` where appropriate).
- Update narrative to reflect that this audit does **not** rely on `ddl-auto=validate` and should be run with `ddl-auto=none` (per the new class Javadoc).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +85 to +89
public static Metadata metadataFor(
final EntityManagerFactory entityManagerFactory) {
return METADATA_BY_UUID.get(entityManagerFactory
.unwrap(SessionFactoryImplementor.class).getUuid());
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

7. Unwrap failures unhandled 🐞 Bug ☼ Reliability

MappingMetadataIntegrator.metadataFor unconditionally calls
entityManagerFactory.unwrap(SessionFactoryImplementor.class); if unwrap is unsupported or fails,
the audit throws an unrelated runtime exception instead of the intended “metadata was not captured”
IllegalStateException.
Agent Prompt
### Issue description
`MappingMetadataIntegrator.metadataFor(...)` can throw during `unwrap(SessionFactoryImplementor.class)` before SchemaEntityValidationAudit can handle the “metadata missing” case, producing confusing failures.

### Issue Context
SchemaEntityValidationAudit.audit() expects `metadataSupplier.get()` to either return Metadata or `null` so it can throw a clear `IllegalStateException` explaining how to enable metadata capture.

### Fix Focus Areas
- src/main/java/io/github/databaseaudits/audit/jpa/MappingMetadataIntegrator.java[85-89]
- src/main/java/io/github/databaseaudits/audit/jpa/SchemaEntityValidationAudit.java[104-112]

### Suggested changes
- In `MappingMetadataIntegrator.metadataFor(...)`:
  - Validate `entityManagerFactory != null`.
  - Wrap `unwrap(...)` in a try/catch (e.g., `RuntimeException`) and either:
    - return `null` (so the audit emits its existing “metadata not captured” message), or
    - throw a new `IllegalStateException` with a targeted message (“EntityManagerFactory is not a Hibernate SessionFactory; cannot resolve UUID for metadata lookup”) and include the caught exception as the cause.
- Optionally, in `SchemaEntityValidationAudit.audit()` catch exceptions thrown by `metadataSupplier.get()` and rethrow as an `IllegalStateException` that preserves the current actionable guidance plus the original cause.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@jeffjensen jeffjensen force-pushed the feat/aggregate-jpa-schema-validation branch from 96e3ddc to 0f893ab Compare July 1, 2026 00:44

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/site/asciidoc/usage.adoc (1)

96-100: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Minor: emf abbreviation breaks naming consistency with other docs pages.

audits.adoc and architecture.adoc both spell out entityManagerFactory; this snippet abbreviates to emf. Purely cosmetic, but consistent naming across pages reads better.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/site/asciidoc/usage.adoc` around lines 96 - 100, The snippet uses the
abbreviated variable name emf, which is inconsistent with the rest of the docs.
Update the example in SchemaEntityValidationAudit.forEntityManagerFactory usage
to spell out entityManagerFactory instead of emf so it matches audits.adoc and
architecture.adoc and keeps naming consistent across pages.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@src/main/java/io/github/databaseaudits/audit/jpa/SchemaEntityValidationAudit.java`:
- Around line 165-186: The schema validation audit only checks unqualified
exclusion keys in SchemaEntityValidationAudit, so schema-specific suppressions
are ignored and same-named tables/columns in other schemas can be hidden
accidentally. Update the exclusion handling in the audit loop to recognize both
table and column exclusions with schema-qualified variants, using the existing
qualifiedName(schema, tableName) flow and the canonical(...) matching logic, so
users can exclude schema.table and schema.table.column precisely.

---

Nitpick comments:
In `@src/site/asciidoc/usage.adoc`:
- Around line 96-100: The snippet uses the abbreviated variable name emf, which
is inconsistent with the rest of the docs. Update the example in
SchemaEntityValidationAudit.forEntityManagerFactory usage to spell out
entityManagerFactory instead of emf so it matches audits.adoc and
architecture.adoc and keeps naming consistent across pages.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 806d2e30-ff55-4d6b-9d87-88d2377860f1

📥 Commits

Reviewing files that changed from the base of the PR and between bebfb2d and 0f893ab.

📒 Files selected for processing (8)
  • src/main/java/io/github/databaseaudits/audit/jpa/MappingMetadataIntegrator.java
  • src/main/java/io/github/databaseaudits/audit/jpa/SchemaEntityValidationAudit.java
  • src/main/resources/META-INF/services/org.hibernate.integrator.spi.Integrator
  • src/site/asciidoc/architecture.adoc
  • src/site/asciidoc/audits.adoc
  • src/site/asciidoc/usage.adoc
  • src/test/java/io/github/databaseaudits/audit/jpa/SchemaEntityValidationAuditIT.java
  • src/test/java/io/github/databaseaudits/audit/jpa/SchemaEntityValidationAuditTest.java

Comment thread src/main/java/io/github/databaseaudits/audit/jpa/SchemaEntityValidationAudit.java Outdated
@jeffjensen jeffjensen force-pushed the feat/aggregate-jpa-schema-validation branch 2 times, most recently from 0f893ab to 9d348d9 Compare July 1, 2026 01:14
SchemaEntityValidationAudit previously only confirmed the EntityManagerFactory
was built, leaning on Hibernate's fail-fast ddl-auto=validate startup check that
aborts on the first mismatch. It now validates every mapped physical table and
column against the live schema itself and returns one finding per missing table,
missing column, and incompatible column type, reproducing Hibernate's own
type-compatibility rule so a mapping Hibernate accepts is never flagged.

A ServiceLoader-registered MappingMetadataIntegrator captures each persistence
unit's boot Metadata (keyed by SessionFactory UUID, so it still resolves through
a proxied EntityManagerFactory such as Spring's); forEntityManagerFactory looks
it back up at audit time.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UkRk16pW4VEM3E6vrDoG5y
@jeffjensen jeffjensen force-pushed the feat/aggregate-jpa-schema-validation branch from 9d348d9 to aefb564 Compare July 1, 2026 03:06
@jeffjensen jeffjensen merged commit 415f454 into main Jul 1, 2026
5 checks passed
@jeffjensen jeffjensen deleted the feat/aggregate-jpa-schema-validation branch July 1, 2026 03:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant