Usage

This page covers direct construction of audits. For Spring Boot, import DatabaseAuditTestConfiguration from the spring-boot-integration module — it wires every audit and collaborator automatically.

Quick start

DatabasePlatform platform = DatabasePlatform.fromDataSource(dataSource);
CatalogQueries queries = new CatalogQueries(dataSource);
IndexCatalog indexes = new IndexCatalog(queries, platform);

assertThat(new PrimaryKeyPresenceAudit(queries, platform)
        .audit("my_schema", PrimaryKeyPresenceAudit.LIQUIBASE_BOOKKEEPING_TABLES))
        .as("Every table should have a primary key.")
        .isEmpty();

assertThat(new ForeignKeyIndexAudit(queries, indexes, platform)
        .audit("my_schema", Set.of()))
        .as("Every foreign key should be backed by an index.")
        .isEmpty();

Each audit() returns List<Finding> — empty when clean, otherwise one Finding per violation, whose description() is the human-readable line. DatabasePlatform supports H2, MARIADB, MYSQL, and POSTGRESQL. See Audits — Catalog for all catalog audits and their exclusion parameters.

Runtime audits

Runtime audits intercept every SQL statement Hibernate executes via SqlCapturingStatementInspector. Most of this page covers the plan-based audits (PostgreSQL 16+ only), which analyze the captured SQL via EXPLAIN (GENERIC_PLAN, FORMAT JSON) and fail immediately on any other platform; an IllegalStateException from one indicates a configuration problem — empty capture or missing preferQueryMode=simple — not a schema violation. Capture-scan audits (e.g. UnconditionalMutationAudit) instead scan the captured SQL text directly and run on every supported platform — see Audits — Capture-scan runtime audits.

JDBC URL requirement

Generic-plan EXPLAIN only works over PostgreSQL’s simple query protocol. Append preferQueryMode=simple to your JDBC URL:

jdbc:postgresql://localhost:5432/mydb?currentSchema=my_schema&preferQueryMode=simple

Without it every parameterized statement is skipped and the vacuous-run guard throws IllegalStateException.

Wiring the SQL capturer

Create one SqlCapturingStatementInspector and pass the object — not a class name — to Hibernate as the StatementInspector. The same instance goes to each runtime audit.

SqlCapturingStatementInspector inspector = new SqlCapturingStatementInspector();

Map<String, Object> settings = new HashMap<>();
settings.put(JdbcSettings.STATEMENT_INSPECTOR, inspector); // the object, not a class name
// ... other Hibernate settings ...

SessionFactory sessionFactory = new Configuration()
        .addProperties(settings)
        .buildSessionFactory();

Registering by class name causes Hibernate to instantiate a separate capturer the audits never read.

For per-test isolation, call inspector.clear() from @BeforeEach — the capture accumulates across the run by default.

Running the workload, then auditing

Run your repository calls first, then audit:

try (Session session = sessionFactory.openSession()) {
    session.createQuery("FROM Order o", Order.class).list();
    session.find(Order.class, 1L);
}

QueryPlanExplainer explainer = new QueryPlanExplainer(dataSource, platform);

assertThat(new WhereClauseIndexAudit(explainer, inspector).audit(Set.of(), List.of()))
        .as("All WHERE clauses should be covered by an index.")
        .isEmpty();

JPA audit

SchemaEntityValidationAudit validates Hibernate’s entity mappings against the live schema, reporting every mismatch (missing table, missing column, incompatible column type) in one run. Run with ddl-auto=none: it does not rely on Hibernate’s fail-fast ddl-auto=validate startup check.

assertThat(SchemaEntityValidationAudit.forEntityManagerFactory(entityManagerFactory, dataSource).audit())
        .as("Entity mappings should match the schema.")
        .isEmpty();

Pass a known, acceptable mismatch as an excluded relation (a table name or table.column, optionally schema-qualified as schema.table / schema.table.column) to audit(Set<String>).