Architecture
Design principles
database-audits-core carries no dependency-injection annotations. Every class takes its dependencies as
constructor arguments, so any container (Spring, Guice, plain new) can wire it. The Spring integration lives
in database-audits-spring-boot, which wires everything with explicit @Bean methods.
Audits return findings; callers assert. Every audit() method returns a List<Finding> — empty when clean.
Each Finding (io.github.databaseaudits.audit.finding) is a record carrying the structured facts behind the
violation plus a description() that is the human-readable line; the calling test makes the assertions
(typically with AssertJ, via Finding::description). This means AssertJ is test-scoped only, never a compile or
runtime dependency of the library.
Never pass vacuously. A run that could not actually check anything throws rather than returning a
clean-looking empty list. Runtime audits throw IllegalStateException on an empty SQL capture and on a
wholly-unexplainable run; plan audits throw UnsupportedOperationException on a non-PostgreSQL platform.
Both errors indicate a configuration problem to fix, not a clean schema.
DatabasePlatform — the hub
DatabasePlatform is an enum of the four supported products: H2, MARIADB, MYSQL, POSTGRESQL. Each
constant holds a CatalogDialect — the source of the catalog audits' per-engine SQL — reached through
platform.catalogDialect():
return platform.catalogDialect().foreignKeysSql();CatalogDialect declares an abstract method for each query whose SQL genuinely diverges between engines
(indexCatalogSql, foreignKeysSql, foreignKeyColumnTypesSql — PostgreSQL’s pg_catalog, MySQL’s
information_schema.statistics/key_column_usage, H2’s information_schema) and a default method for each
query whose standard information_schema SQL every engine shares (tablesWithoutPrimaryKeySql,
nullableForeignKeyColumnSql). PostgresqlCatalogDialect, MysqlCatalogDialect, and H2CatalogDialect supply
the divergent SQL; MARIADB reuses MysqlCatalogDialect. Adding an engine means adding an enum constant with a
dialect: the constant’s constructor requires one, and a divergent dialect will not compile until it implements
every abstract method — the same compile-time completeness the old exhaustive switch`es gave, while the shared
SQL is written once. Detect the platform once at startup with `DatabasePlatform.fromDataSource(DataSource),
which opens one connection, reads DatabaseMetaData.getDatabaseProductName(), and closes the connection.
Catalog family (all platforms)
Eight audits that query database metadata — information_schema or pg_catalog — over plain JDBC and compare
it against rules. Results are deterministic regardless of test data; no SQL execution is analyzed.
CatalogQueries is the minimal JDBC-to-list-of-maps layer. It prepares one parameterized statement per call
and returns rows as Map<String, Object> with case-insensitive key lookup — necessary because PostgreSQL
lower-cases unquoted aliases while H2 upper-cases them.
IndexCatalog reads every index of a schema as IndexDefinition records (key columns in order, excluding
INCLUDE columns and full-text/spatial indexes). It is the shared building block for ForeignKeyIndexAudit
(leading-prefix coverage) and RedundantIndexAudit (prefix containment).
IndexDefinition is a value record holding a table name, index name, ordered column list, and flags for
unique, primary, partial, and expression/prefix columns.
ForeignKeyCatalog reads every foreign key of a schema as ForeignKeyDefinition records (columns and
referenced columns paired in constraint order), reusing CatalogDialect.foreignKeyColumnTypesSql() rather than
a dedicated query. It is the shared building block for DuplicateForeignKeyAudit (relationship grouping).
Runtime family
Seven audits that inspect the real SQL Hibernate executed during the test run, via
SqlCapturingStatementInspector. Four are Query plan analysis (PostgreSQL 16+ only) (PostgreSQL 16+ only, via EXPLAIN); three are
Capture-scan audits (every platform) (every platform, token-scanning the captured SQL text).
SQL capture
SqlCapturingStatementInspector implements Hibernate’s StatementInspector, recording every statement with
JDBC ? placeholders (no bind values) — exactly what EXPLAIN (GENERIC_PLAN) needs. The same instance must
be both Hibernate’s inspector and the one the runtime audits read; see
Usage — Runtime audits for wiring.
Query plan analysis (PostgreSQL 16+ only)
QueryPlanExplainer runs EXPLAIN (GENERIC_PLAN, FORMAT JSON) with chosen planner-penalty settings (for
example SET enable_seqscan = off). A surviving node of the penalized kind proves no index can serve that
access path, without needing real bind values or test data. Requires preferQueryMode=simple on the JDBC URL —
generic-plan EXPLAIN only works over PostgreSQL’s simple query protocol.
Template method
CapturedSqlPlanAuditTemplate is the base class for three of the four EXPLAIN-based audits
(WhereClauseIndexAudit, OrderByIndexAudit, JoinIndexAudit). The fixed algorithm de-duplicates captured SQL
by normalized statement shape, plans each candidate with penalties applied, and collects offending nodes. Both
vacuous-run guards — empty capture and wholly-unexplainable run — live here. Subclasses supply which statements
to EXPLAIN, which GUCs to penalize, and how to recognize an offending node.
UnusedIndexAudit is the fourth plan-based audit, and deliberately does not extend the template: the
template emits one finding per offending statement, but this audit needs the union of index usage across
every statement, then a diff against the catalog — the inverse shape. It plans each candidate via the natural
generic plan (no penalties), collects every Index Name the plan mentions, and reports each catalog index
(from IndexCatalog) that is neither justified by a primary key, a unique constraint, being partial, appearing
in that usage, nor covering a foreign key (via ForeignKeyCatalog).
Capture-scan audits (every platform)
These three need no EXPLAIN and no PostgreSQL-specific technique — they token-scan the normalized captured SQL
text directly. UnconditionalMutationAudit scans for UPDATE/DELETE without WHERE.
OffsetPaginationAudit scans for OFFSET-based pagination (or MySQL/MariaDB’s comma
LIMIT <offset>, <count> form), which scales linearly with page depth. RepeatedStatementAudit reads
SqlCapturingStatementInspector.executionCounts() (a per-statement tally the capturer keeps alongside the
capture set) to catch N+1 statement bursts — a SELECT shape captured at least a given threshold number of
times.
JPA family
SchemaEntityValidationAudit validates Hibernate’s entity mappings against the live schema, reporting every
missing table, missing column, and incompatible column type in one run (using Hibernate’s own type-compatibility
rule). It runs under ddl-auto=none — not Hibernate’s fail-fast ddl-auto=validate — obtaining the boot mapping
model from a MappingMetadataIntegrator captured during bootstrap, plus a DataSource to read the live schema.
MissingVersionAttributeAudit walks the same boot mapping model for mutable root entities with no
@Version attribute — a lost-update risk under concurrent transactions. It needs no DataSource: only the
mapping model, never the live schema.
EagerCollectionFetchAudit walks the boot mapping model’s collection bindings (org.hibernate.mapping.Collection,
covering both entity associations and @ElementCollection`s) for any fetched eagerly. Also needs no
`DataSource.
UnmappedDatabaseObjectAudit is SchemaEntityValidationAudit’s mirror image: instead of checking every
mapping exists in the schema, it checks the schema holds only what is mapped. It builds the mapped table/column
sets from `Metadata.collectTableMappings() (which includes Hibernate-generated join/collection tables, not just
each entity’s primary table) and diffs them against DatabaseMetaData.getTables()/getColumns(), scanning only
schemas that contain at least one mapped table.
Exclusions
Every audit accepts exclusion parameters so consumers suppress known-intentional violations rather than making
the audit guess. Exclusion types vary by audit — table names, constraint names, table.column identifiers,
index names, relation names, SQL fragments, or normalized statement strings. See
Audits for each audit’s exclusion type.

