Exclusions Guide

Audits are strictest by default. When a violation is intentional — a legacy table without a primary key, a nullable FK for a nullable relationship, a known full-table migration statement — exclude it rather than weakening the audit. Exclusions are named and visible in the test source, making the intent explicit.

When to exclude vs. when to fix

Fix the schema when the audit finding reflects a genuine structural problem (missing index, type mismatch).

Exclude when:

  • A Liquibase migration or one-off script intentionally runs a full-table UPDATE/DELETE.
  • A relation is known to be small enough that a sequential scan is appropriate and intentional.
  • A FK is intentionally nullable (the relationship is optional).
  • A third-party table (e.g., from an embedded library) cannot be modified.

The DatabaseAuditExcludes builder

DatabaseAuditExcludes is an immutable value object. Use DatabaseAuditExcludes.none() for no exclusions, or the fluent builder to name each exclusion:

DatabaseAuditExcludes excludes = DatabaseAuditExcludes.builder()
    .primaryKeyTables(Set.of("legacy_table_no_pk"))
    .primaryKeyTypeColumns(Set.of("lookup_values.id"))       // table.column
    .duplicateForeignKeyConstraints(Set.of("fk_orders_customer_legacy"))
    .foreignKeyIndexConstraints(Set.of("fk_unindexed_by_design"))
    .foreignKeyNotNullColumns(Set.of("order.customer_id"))   // table.column
    .foreignKeyTypeMatchColumns(Set.of("event.entity_id"))   // table.column
    .redundantIndexes(Set.of("idx_status_redundant"))
    .uniqueIndexNotNullIndexes(Set.of("uq_orders_active_customer"))
    .jpaExcludedRelations(Set.of("legacy_reporting_view"))   // table or table.column, case-insensitive
    .unmappedDatabaseObjectRelations(Set.of("databasechangelog", "databasechangeloglock"))
    .missingVersionEntities(Set.of("AuditLogEntry"))         // fully-qualified name, simple name, or table
    .eagerCollectionRoles(Set.of("com.acme.Order.items"))
    .planRelations(Set.of("small_lookup_table"))             // shared by WHERE/ORDER BY/JOIN audits
    .planSqlFragments(List.of("UPDATE migration_flag"))      // matched as substring
    .unusedIndexes(Set.of("idx_orders_admin_report"))
    .unconditionalMutationStatements(Set.of("DELETE FROM scratch_pad")) // normalized, case-insensitive
    .offsetPaginationSqlFragments(List.of("from admin_report"))
    .repeatedStatementThreshold(50)                          // a generous regression tripwire, not a precise count
    .repeatedStatementSqlFragments(List.of("from feature_flags"))
    .build();

Pass the exclusions to the facade:

audits.assertCatalogClean(schema, excludes);
audits.assertRuntimeClean(excludes);
audits.assertAllClean(schema, excludes);

Per-family exclusion reference

Catalog exclusions

Builder method Applies to Match semantics
primaryKeyTables(Set<String>) PrimaryKeyPresenceAuditAssertion Exact table name.
primaryKeyTypeColumns(Set<String>) PrimaryKeyTypeAuditAssertion Case-insensitive table.column string.
duplicateForeignKeyConstraints(Set<String>) DuplicateForeignKeyAuditAssertion Exact FK constraint name. Excluding one constraint of a duplicate pair drops that relationship’s group below two members, suppressing the finding entirely.
foreignKeyIndexConstraints(Set<String>) ForeignKeyIndexAuditAssertion Exact FK constraint name.
foreignKeyNotNullColumns(Set<String>) ForeignKeyNotNullAuditAssertion Exact table.column string.
foreignKeyTypeMatchColumns(Set<String>) ForeignKeyTypeMatchAuditAssertion Exact table.column string.
redundantIndexes(Set<String>) RedundantIndexAuditAssertion Exact index name.
uniqueIndexNotNullIndexes(Set<String>) UniqueIndexNotNullAuditAssertion Exact index name.

PrimaryKeyPresenceAuditAssertion also excludes the Liquibase bookkeeping tables (databasechangelog, databasechangeloglock) automatically, without needing an explicit exclusion. PrimaryKeyTypeAuditAssertion and UnmappedDatabaseObjectAuditAssertion do not — since databasechangeloglock’s primary key is a narrow `INT and neither bookkeeping table is entity-mapped, exclude them explicitly (see the archetype example ITs).

JPA exclusions

Builder method Applies to Match semantics
jpaExcludedRelations(Set<String>) SchemaEntityValidationAuditAssertion A table or table.column name — optionally schema-qualified as schema.table / schema.table.column — matched case-insensitively. Suppresses a known, acceptable entity/schema mismatch.
unmappedDatabaseObjectRelations(Set<String>) UnmappedDatabaseObjectAuditAssertion A table or table.column name — optionally schema-qualified — matched case-insensitively. Suppresses a known, acceptable unmapped relation (e.g. a migration-tool bookkeeping table).
missingVersionEntities(Set<String>) MissingVersionAttributeAuditAssertion An entity’s fully-qualified name, simple name, or physical table name — matched case-insensitively.
eagerCollectionRoles(Set<String>) EagerCollectionFetchAuditAssertion Exact Hibernate collection role (e.g. com.acme.Order.items), matched case-insensitively.

Runtime exclusions

Builder method Applies to Match semantics
planRelations(Set<String>) WhereClauseIndexAuditAssertion, OrderByIndexAuditAssertion, JoinIndexAuditAssertion Exact relation (table) name. Any statement touching that relation is skipped.
planSqlFragments(List<String>) WhereClauseIndexAuditAssertion, OrderByIndexAuditAssertion, JoinIndexAuditAssertion Substring match against the full SQL statement.
unusedIndexes(Set<String>) UnusedIndexAuditAssertion Exact index name.
unconditionalMutationStatements(Set<String>) UnconditionalMutationAuditAssertion Case-insensitive match against the normalized statement text.
offsetPaginationSqlFragments(List<String>) OffsetPaginationAuditAssertion Case-insensitive substring match against the normalized statement text.
repeatedStatementThreshold(int) RepeatedStatementAuditAssertion The minimum capture count (inclusive, at least 2) for a SELECT shape to be reported; defaults to a generous 50 as a regression tripwire rather than a precise N+1 count.
repeatedStatementSqlFragments(List<String>) RepeatedStatementAuditAssertion Case-insensitive substring match against the normalized statement text.

Passing exclusions to individual assertion beans

Each assertion bean also accepts exclusions directly on its assertClean(…​) overload:

// Catalog: pass a Set of excluded table names
primaryKeyPresenceAuditAssertion.assertClean(schema, Set.of("legacy_table_no_pk"));

// Catalog: pass a Set of excluded FK constraint names
foreignKeyIndexAuditAssertion.assertClean(schema, Set.of("fk_unindexed_by_design"));

// Runtime: pass excluded relations and SQL fragments separately
whereClauseIndexAuditAssertion.assertClean(
    Set.of("small_lookup_table"),
    List.of("UPDATE migration_flag")
);

// Runtime: pass exact statements to exclude from the mutation audit
unconditionalMutationAuditAssertion.assertClean(Set.of("DELETE FROM scratch_pad"));

Using the DatabaseAuditExcludes builder and the facade is preferred when running multiple audits together, since it keeps all exclusions in one place.