DatabaseAuditExcludes.java

package io.github.databaseaudits.spring.boot.assertion;

import java.util.List;
import java.util.Set;

/**
 * Immutable exclusions for the {@link DatabaseAuditAssertions} facade. Every
 * field defaults to empty (or, for {@code repeatedStatementThreshold}, a
 * generous default); build with {@link #builder()} or use {@link #none()}.
 * The plan exclusions ({@code planRelations}, {@code planSqlFragments}) apply
 * to all three plan-based audits (join, order-by, where); for per-audit
 * exclusions inject the individual {@code *AuditAssertion} beans instead.
 */
public class DatabaseAuditExcludes {
    /**
     * Default {@code RepeatedStatementAudit} threshold — generous enough to
     * act as a regression tripwire rather than a precise N+1 count.
     */
    private static final int DEFAULT_REPEATED_STATEMENT_THRESHOLD = 50;

    private static final DatabaseAuditExcludes NONE = builder().build();

    private final Set<String> duplicateForeignKeyConstraints;
    private final Set<String> eagerCollectionRoles;
    private final Set<String> foreignKeyIndexConstraints;
    private final Set<String> foreignKeyNotNullColumns;
    private final Set<String> foreignKeyTypeMatchColumns;
    private final Set<String> jpaExcludedRelations;
    private final Set<String> missingVersionEntities;
    private final List<String> offsetPaginationSqlFragments;
    private final Set<String> planRelations;
    private final List<String> planSqlFragments;
    private final Set<String> primaryKeyTables;
    private final Set<String> primaryKeyTypeColumns;
    private final Set<String> redundantIndexes;
    private final List<String> repeatedStatementSqlFragments;
    private final int repeatedStatementThreshold;
    private final Set<String> unconditionalMutationStatements;
    private final Set<String> uniqueIndexNotNullIndexes;
    private final Set<String> unmappedDatabaseObjectRelations;
    private final Set<String> unusedIndexes;

    private DatabaseAuditExcludes(final Builder builder) {
        this.duplicateForeignKeyConstraints =
                builder.duplicateForeignKeyConstraints;
        this.eagerCollectionRoles = builder.eagerCollectionRoles;
        this.foreignKeyIndexConstraints = builder.foreignKeyIndexConstraints;
        this.foreignKeyNotNullColumns = builder.foreignKeyNotNullColumns;
        this.foreignKeyTypeMatchColumns = builder.foreignKeyTypeMatchColumns;
        this.jpaExcludedRelations = builder.jpaExcludedRelations;
        this.missingVersionEntities = builder.missingVersionEntities;
        this.offsetPaginationSqlFragments =
                builder.offsetPaginationSqlFragments;
        this.planRelations = builder.planRelations;
        this.planSqlFragments = builder.planSqlFragments;
        this.primaryKeyTables = builder.primaryKeyTables;
        this.primaryKeyTypeColumns = builder.primaryKeyTypeColumns;
        this.redundantIndexes = builder.redundantIndexes;
        this.repeatedStatementSqlFragments =
                builder.repeatedStatementSqlFragments;
        this.repeatedStatementThreshold = builder.repeatedStatementThreshold;
        this.unconditionalMutationStatements =
                builder.unconditionalMutationStatements;
        this.uniqueIndexNotNullIndexes = builder.uniqueIndexNotNullIndexes;
        this.unmappedDatabaseObjectRelations =
                builder.unmappedDatabaseObjectRelations;
        this.unusedIndexes = builder.unusedIndexes;
    }

    /**
     * Returns an empty set of exclusions.
     *
     * @return the empty exclusions.
     */
    public static DatabaseAuditExcludes none() {
        return NONE;
    }

    /**
     * Returns a new builder whose exclusions all default to empty (or, for
     * {@code repeatedStatementThreshold}, the generous default).
     *
     * @return the builder.
     */
    public static Builder builder() {
        return new Builder();
    }

    Set<String> duplicateForeignKeyConstraints() {
        return duplicateForeignKeyConstraints;
    }

    Set<String> eagerCollectionRoles() {
        return eagerCollectionRoles;
    }

    Set<String> foreignKeyIndexConstraints() {
        return foreignKeyIndexConstraints;
    }

    Set<String> foreignKeyNotNullColumns() {
        return foreignKeyNotNullColumns;
    }

    Set<String> foreignKeyTypeMatchColumns() {
        return foreignKeyTypeMatchColumns;
    }

    Set<String> jpaExcludedRelations() {
        return jpaExcludedRelations;
    }

    Set<String> missingVersionEntities() {
        return missingVersionEntities;
    }

    List<String> offsetPaginationSqlFragments() {
        return offsetPaginationSqlFragments;
    }

    Set<String> planRelations() {
        return planRelations;
    }

    List<String> planSqlFragments() {
        return planSqlFragments;
    }

    Set<String> primaryKeyTables() {
        return primaryKeyTables;
    }

    Set<String> primaryKeyTypeColumns() {
        return primaryKeyTypeColumns;
    }

    Set<String> redundantIndexes() {
        return redundantIndexes;
    }

    List<String> repeatedStatementSqlFragments() {
        return repeatedStatementSqlFragments;
    }

    int repeatedStatementThreshold() {
        return repeatedStatementThreshold;
    }

    Set<String> unconditionalMutationStatements() {
        return unconditionalMutationStatements;
    }

    Set<String> uniqueIndexNotNullIndexes() {
        return uniqueIndexNotNullIndexes;
    }

    Set<String> unmappedDatabaseObjectRelations() {
        return unmappedDatabaseObjectRelations;
    }

    Set<String> unusedIndexes() {
        return unusedIndexes;
    }

    /**
     * Builder for {@link DatabaseAuditExcludes}.
     */
    public static final class Builder {
        private Set<String> duplicateForeignKeyConstraints = Set.of();
        private Set<String> eagerCollectionRoles = Set.of();
        private Set<String> foreignKeyIndexConstraints = Set.of();
        private Set<String> foreignKeyNotNullColumns = Set.of();
        private Set<String> foreignKeyTypeMatchColumns = Set.of();
        private Set<String> jpaExcludedRelations = Set.of();
        private Set<String> missingVersionEntities = Set.of();
        private List<String> offsetPaginationSqlFragments = List.of();
        private Set<String> planRelations = Set.of();
        private List<String> planSqlFragments = List.of();
        private Set<String> primaryKeyTables = Set.of();
        private Set<String> primaryKeyTypeColumns = Set.of();
        private Set<String> redundantIndexes = Set.of();
        private List<String> repeatedStatementSqlFragments = List.of();
        private int repeatedStatementThreshold =
                DEFAULT_REPEATED_STATEMENT_THRESHOLD;
        private Set<String> unconditionalMutationStatements = Set.of();
        private Set<String> uniqueIndexNotNullIndexes = Set.of();
        private Set<String> unmappedDatabaseObjectRelations = Set.of();
        private Set<String> unusedIndexes = Set.of();

        /** Creates a builder with every exclusion defaulting to empty. */
        public Builder() {
        }

        /**
         * Excludes constraints from the duplicate-foreign-key audit, leaving
         * the duplication in place deliberately.
         *
         * @param constraints
         *                        the FK constraint names to exclude.
         * @return this builder.
         */
        public Builder duplicateForeignKeyConstraints(
                final Set<String> constraints) {
            this.duplicateForeignKeyConstraints = constraints;
            return this;
        }

        /**
         * Excludes deliberately-eager collection roles from the
         * eager-collection-fetch audit.
         *
         * @param roles
         *                  the collection roles to exclude (e.g.
         *                  {@code com.acme.Order.items}).
         * @return this builder.
         */
        public Builder eagerCollectionRoles(final Set<String> roles) {
            this.eagerCollectionRoles = roles;
            return this;
        }

        /**
         * Excludes intentionally-unindexed FK constraints from the foreign-key
         * index audit.
         *
         * @param constraints
         *                        the FK constraint names to exclude.
         * @return this builder.
         */
        public Builder foreignKeyIndexConstraints(
                final Set<String> constraints) {
            this.foreignKeyIndexConstraints = constraints;
            return this;
        }

        /**
         * Excludes genuinely-optional foreign key columns from the not-null
         * audit.
         *
         * @param columns
         *                    the {@code table.column} names to exclude.
         * @return this builder.
         */
        public Builder foreignKeyNotNullColumns(final Set<String> columns) {
            this.foreignKeyNotNullColumns = columns;
            return this;
        }

        /**
         * Excludes deliberate type mismatches from the foreign-key type-match
         * audit.
         *
         * @param columns
         *                    the {@code table.column} names to exclude.
         * @return this builder.
         */
        public Builder foreignKeyTypeMatchColumns(final Set<String> columns) {
            this.foreignKeyTypeMatchColumns = columns;
            return this;
        }

        /**
         * Excludes relations (tables or {@code table.column} pairs) from the
         * JPA schema/entity validation audit.
         *
         * @param relations
         *                      the relation names to exclude, matched
         *                      case-insensitively.
         * @return this builder.
         */
        public Builder jpaExcludedRelations(final Set<String> relations) {
            this.jpaExcludedRelations = relations;
            return this;
        }

        /**
         * Excludes append-only or single-writer entities from the
         * missing-version-attribute audit.
         *
         * @param entities
         *                     the entities to exclude — fully-qualified name,
         *                     simple name, or physical table name.
         * @return this builder.
         */
        public Builder missingVersionEntities(final Set<String> entities) {
            this.missingVersionEntities = entities;
            return this;
        }

        /**
         * Excludes deliberately shallow/bounded paginated statements from the
         * offset-pagination audit.
         *
         * @param sqlFragments
         *                         the SQL fragments to exclude.
         * @return this builder.
         */
        public Builder offsetPaginationSqlFragments(
                final List<String> sqlFragments) {
            this.offsetPaginationSqlFragments = sqlFragments;
            return this;
        }

        /**
         * Excludes relations from all three plan-based audits (join, order-by,
         * where).
         *
         * @param relations
         *                      the relation names to exclude.
         * @return this builder.
         */
        public Builder planRelations(final Set<String> relations) {
            this.planRelations = relations;
            return this;
        }

        /**
         * Excludes statements containing any of these SQL fragments from all
         * three plan-based audits.
         *
         * @param sqlFragments
         *                         the SQL fragments to exclude.
         * @return this builder.
         */
        public Builder planSqlFragments(final List<String> sqlFragments) {
            this.planSqlFragments = sqlFragments;
            return this;
        }

        /**
         * Excludes tables from the primary-key presence audit (the Liquibase
         * bookkeeping tables are always excluded in addition to these).
         *
         * @param tables
         *                   the table names to exclude.
         * @return this builder.
         */
        public Builder primaryKeyTables(final Set<String> tables) {
            this.primaryKeyTables = tables;
            return this;
        }

        /**
         * Excludes genuinely-bounded tables' primary key columns from the
         * primary-key-type audit.
         *
         * @param columns
         *                    the {@code table.column} names to exclude.
         * @return this builder.
         */
        public Builder primaryKeyTypeColumns(final Set<String> columns) {
            this.primaryKeyTypeColumns = columns;
            return this;
        }

        /**
         * Excludes intentional look-alike indexes from the redundant-index
         * audit.
         *
         * @param indexes
         *                    the index names to exclude.
         * @return this builder.
         */
        public Builder redundantIndexes(final Set<String> indexes) {
            this.redundantIndexes = indexes;
            return this;
        }

        /**
         * Excludes legitimately-hot statements from the repeated-statement
         * (N+1) audit.
         *
         * @param sqlFragments
         *                         the SQL fragments to exclude.
         * @return this builder.
         */
        public Builder repeatedStatementSqlFragments(
                final List<String> sqlFragments) {
            this.repeatedStatementSqlFragments = sqlFragments;
            return this;
        }

        /**
         * Sets the minimum capture count (inclusive) for a SELECT shape to be
         * reported by the repeated-statement (N+1) audit; must be at least 2.
         * Defaults to {@value #DEFAULT_REPEATED_STATEMENT_THRESHOLD}, a
         * generous regression tripwire rather than a precise count.
         *
         * @param threshold
         *                      the minimum capture count.
         * @return this builder.
         * @throws IllegalArgumentException
         *                                      if {@code threshold} is less
         *                                      than 2.
         */
        public Builder repeatedStatementThreshold(final int threshold) {
            if (threshold < 2) {
                throw new IllegalArgumentException(
                        "repeatedStatementThreshold must be at least 2, was "
                                + threshold);
            }
            this.repeatedStatementThreshold = threshold;
            return this;
        }

        /**
         * Excludes deliberate full-table statements from the
         * unconditional-mutation audit.
         *
         * @param statements
         *                       the statements to exclude, matched
         *                       case-insensitively against the normalized
         *                       statement text.
         * @return this builder.
         */
        public Builder unconditionalMutationStatements(
                final Set<String> statements) {
            this.unconditionalMutationStatements = statements;
            return this;
        }

        /**
         * Excludes indexes with deliberate partial uniqueness from the
         * unique-index-not-null audit.
         *
         * @param indexes
         *                    the index names to exclude.
         * @return this builder.
         */
        public Builder uniqueIndexNotNullIndexes(final Set<String> indexes) {
            this.uniqueIndexNotNullIndexes = indexes;
            return this;
        }

        /**
         * Excludes known, acceptable unmapped relations (tables or
         * {@code table.column} pairs, optionally schema-qualified) from the
         * unmapped-database-object audit — for example migration-tool
         * bookkeeping tables.
         *
         * @param relations
         *                      the relation names to exclude, matched
         *                      case-insensitively.
         * @return this builder.
         */
        public Builder unmappedDatabaseObjectRelations(
                final Set<String> relations) {
            this.unmappedDatabaseObjectRelations = relations;
            return this;
        }

        /**
         * Excludes indexes kept for a workload outside the captured one (e.g.
         * a rare admin query) from the unused-index audit.
         *
         * @param indexes
         *                    the index names to exclude.
         * @return this builder.
         */
        public Builder unusedIndexes(final Set<String> indexes) {
            this.unusedIndexes = indexes;
            return this;
        }

        /**
         * Builds the exclusions.
         *
         * @return the exclusions.
         */
        public DatabaseAuditExcludes build() {
            return new DatabaseAuditExcludes(this);
        }
    }
}