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>).

