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<String> — empty when clean, otherwise human-readable violation messages. DatabasePlatform supports H2, MARIADB, MYSQL, and POSTGRESQL. See Audits — Catalog for all catalog audits and their exclusion parameters.

Runtime audits (PostgreSQL 16+ only)

Runtime audits intercept the SQL Hibernate executes and analyze it via EXPLAIN (GENERIC_PLAN, FORMAT JSON). They are PostgreSQL-only and fail immediately on any other platform. An IllegalStateException from a runtime audit indicates a configuration problem — empty capture or missing preferQueryMode=simple — not a schema violation.

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 is a formality: when ddl-auto=validate is active, Hibernate validates every mapped entity at EntityManagerFactory build time — reaching the audit means validation passed.

Map<String, Object> settings = new HashMap<>();
settings.put("hibernate.hbm2ddl.auto", "validate");
EntityManagerFactory emf = Persistence.createEntityManagerFactory("my-unit", settings);

assertThat(new SchemaEntityValidationAudit(emf).audit())
        .as("Entity mappings should match the schema.")
        .isEmpty();

The audit is only meaningful when the test enables validation.