From eba2fc6fd18d8d6b053be682986e3834e1bbdf2b Mon Sep 17 00:00:00 2001 From: Ivan Vakhrushev Date: Mon, 11 Nov 2024 23:42:05 +0400 Subject: [PATCH] Add SkipIndexesByNamePredicate (#490) * Add SkipIndexesByNamePredicate * Refactor tests --- .../host/IndexesWithBloatCheckOnHostTest.java | 5 + .../IndexesWithBooleanCheckOnHostTest.java | 5 + .../IndexesWithNullValuesCheckOnHostTest.java | 5 + .../host/InvalidIndexesCheckOnHostTest.java | 5 + .../host/UnusedIndexesCheckOnHostTest.java | 6 +- .../AbstractSkipTablesPredicate.java | 44 +++++-- .../SkipBySequenceNamePredicate.java | 14 +-- .../SkipDbObjectsByNamePredicate.java | 3 +- .../SkipIndexesByNamePredicate.java | 111 +++++++++++++++++ .../predicates/SkipTablesByNamePredicate.java | 4 +- .../AbstractSkipTablesPredicateTest.java | 2 +- .../SkipBySequenceNamePredicateTest.java | 4 +- .../SkipIndexesByNamePredicateTest.java | 117 ++++++++++++++++++ .../SkipTablesByNamePredicateTest.java | 4 +- .../FilterIndexesByNamePredicate.java | 2 + .../IndexesWithBloatCheckOnClusterTest.java | 23 ++-- .../IndexesWithBooleanCheckOnClusterTest.java | 9 +- ...dexesWithNullValuesCheckOnClusterTest.java | 9 +- .../InvalidIndexesCheckOnClusterTest.java | 9 +- .../UnusedIndexesCheckOnClusterTest.java | 25 ++-- 20 files changed, 343 insertions(+), 63 deletions(-) create mode 100644 pg-index-health-model/src/main/java/io/github/mfvanek/pg/model/predicates/SkipIndexesByNamePredicate.java create mode 100644 pg-index-health-model/src/test/java/io/github/mfvanek/pg/model/predicates/SkipIndexesByNamePredicateTest.java diff --git a/pg-index-health-core/src/test/java/io/github/mfvanek/pg/checks/host/IndexesWithBloatCheckOnHostTest.java b/pg-index-health-core/src/test/java/io/github/mfvanek/pg/checks/host/IndexesWithBloatCheckOnHostTest.java index 34c464c5..00290551 100644 --- a/pg-index-health-core/src/test/java/io/github/mfvanek/pg/checks/host/IndexesWithBloatCheckOnHostTest.java +++ b/pg-index-health-core/src/test/java/io/github/mfvanek/pg/checks/host/IndexesWithBloatCheckOnHostTest.java @@ -14,6 +14,7 @@ import io.github.mfvanek.pg.common.maintenance.Diagnostic; import io.github.mfvanek.pg.model.PgContext; import io.github.mfvanek.pg.model.index.IndexWithBloat; +import io.github.mfvanek.pg.model.predicates.SkipIndexesByNamePredicate; import io.github.mfvanek.pg.model.predicates.SkipTablesByNamePredicate; import io.github.mfvanek.pg.support.StatisticsAwareTestBase; import org.assertj.core.api.Assertions; @@ -62,6 +63,10 @@ void onDatabaseWithThem(final String schemaName) { assertThat(check) .executing(ctx, SkipTablesByNamePredicate.of(ctx, List.of("accounts", "clients"))) .isEmpty(); + + assertThat(check) + .executing(ctx, SkipIndexesByNamePredicate.of(ctx, List.of("accounts_account_number_key", "accounts_pkey", "clients_pkey", "i_clients_email_phone"))) + .isEmpty(); }); } } diff --git a/pg-index-health-core/src/test/java/io/github/mfvanek/pg/checks/host/IndexesWithBooleanCheckOnHostTest.java b/pg-index-health-core/src/test/java/io/github/mfvanek/pg/checks/host/IndexesWithBooleanCheckOnHostTest.java index d2386b5c..ba3d5280 100644 --- a/pg-index-health-core/src/test/java/io/github/mfvanek/pg/checks/host/IndexesWithBooleanCheckOnHostTest.java +++ b/pg-index-health-core/src/test/java/io/github/mfvanek/pg/checks/host/IndexesWithBooleanCheckOnHostTest.java @@ -15,6 +15,7 @@ import io.github.mfvanek.pg.model.PgContext; import io.github.mfvanek.pg.model.column.Column; import io.github.mfvanek.pg.model.index.IndexWithColumns; +import io.github.mfvanek.pg.model.predicates.SkipIndexesByNamePredicate; import io.github.mfvanek.pg.model.predicates.SkipTablesByNamePredicate; import io.github.mfvanek.pg.support.DatabaseAwareTestBase; import org.junit.jupiter.api.Test; @@ -51,6 +52,10 @@ void onDatabaseWithThem(final String schemaName) { assertThat(check) .executing(ctx, SkipTablesByNamePredicate.ofName(ctx, "accounts")) .isEmpty(); + + assertThat(check) + .executing(ctx, SkipIndexesByNamePredicate.ofName(ctx, "i_accounts_deleted")) + .isEmpty(); }); } } diff --git a/pg-index-health-core/src/test/java/io/github/mfvanek/pg/checks/host/IndexesWithNullValuesCheckOnHostTest.java b/pg-index-health-core/src/test/java/io/github/mfvanek/pg/checks/host/IndexesWithNullValuesCheckOnHostTest.java index f5cc6f91..c4630a08 100644 --- a/pg-index-health-core/src/test/java/io/github/mfvanek/pg/checks/host/IndexesWithNullValuesCheckOnHostTest.java +++ b/pg-index-health-core/src/test/java/io/github/mfvanek/pg/checks/host/IndexesWithNullValuesCheckOnHostTest.java @@ -14,6 +14,7 @@ import io.github.mfvanek.pg.common.maintenance.Diagnostic; import io.github.mfvanek.pg.model.PgContext; import io.github.mfvanek.pg.model.index.IndexWithNulls; +import io.github.mfvanek.pg.model.predicates.SkipIndexesByNamePredicate; import io.github.mfvanek.pg.model.predicates.SkipTablesByNamePredicate; import io.github.mfvanek.pg.support.DatabaseAwareTestBase; import org.junit.jupiter.api.Test; @@ -49,6 +50,10 @@ void onDatabaseWithThem(final String schemaName) { assertThat(check) .executing(ctx, SkipTablesByNamePredicate.ofName(ctx, "clients")) .isEmpty(); + + assertThat(check) + .executing(ctx, SkipIndexesByNamePredicate.ofName(ctx, "i_clients_middle_name")) + .isEmpty(); }); } } diff --git a/pg-index-health-core/src/test/java/io/github/mfvanek/pg/checks/host/InvalidIndexesCheckOnHostTest.java b/pg-index-health-core/src/test/java/io/github/mfvanek/pg/checks/host/InvalidIndexesCheckOnHostTest.java index cf336cea..870ed907 100644 --- a/pg-index-health-core/src/test/java/io/github/mfvanek/pg/checks/host/InvalidIndexesCheckOnHostTest.java +++ b/pg-index-health-core/src/test/java/io/github/mfvanek/pg/checks/host/InvalidIndexesCheckOnHostTest.java @@ -14,6 +14,7 @@ import io.github.mfvanek.pg.common.maintenance.Diagnostic; import io.github.mfvanek.pg.model.PgContext; import io.github.mfvanek.pg.model.index.Index; +import io.github.mfvanek.pg.model.predicates.SkipIndexesByNamePredicate; import io.github.mfvanek.pg.model.predicates.SkipTablesByNamePredicate; import io.github.mfvanek.pg.support.DatabaseAwareTestBase; import org.junit.jupiter.api.Test; @@ -48,6 +49,10 @@ void onDatabaseWithThem(final String schemaName) { assertThat(check) .executing(ctx, SkipTablesByNamePredicate.ofName(ctx, "clients")) .isEmpty(); + + assertThat(check) + .executing(ctx, SkipIndexesByNamePredicate.ofName(ctx, "i_clients_last_name_first_name")) + .isEmpty(); }); } } diff --git a/pg-index-health-core/src/test/java/io/github/mfvanek/pg/checks/host/UnusedIndexesCheckOnHostTest.java b/pg-index-health-core/src/test/java/io/github/mfvanek/pg/checks/host/UnusedIndexesCheckOnHostTest.java index ca90387f..aa401397 100644 --- a/pg-index-health-core/src/test/java/io/github/mfvanek/pg/checks/host/UnusedIndexesCheckOnHostTest.java +++ b/pg-index-health-core/src/test/java/io/github/mfvanek/pg/checks/host/UnusedIndexesCheckOnHostTest.java @@ -15,6 +15,7 @@ import io.github.mfvanek.pg.model.PgContext; import io.github.mfvanek.pg.model.index.UnusedIndex; import io.github.mfvanek.pg.model.predicates.SkipDbObjectsByNamePredicate; +import io.github.mfvanek.pg.model.predicates.SkipIndexesByNamePredicate; import io.github.mfvanek.pg.model.predicates.SkipTablesByNamePredicate; import io.github.mfvanek.pg.support.DatabaseAwareTestBase; import org.junit.jupiter.api.Test; @@ -56,8 +57,9 @@ void onDatabaseWithThem(final String schemaName) { .allMatch(i -> i.getIndexScans() == 0); assertThat(check) - .executing(ctx, SkipTablesByNamePredicate.ofName(ctx, "accounts")) - .hasSize(2); + .executing(ctx, SkipTablesByNamePredicate.ofName(ctx, "accounts") + .and(SkipIndexesByNamePredicate.of(ctx, List.of("i_clients_last_first", "i_clients_last_name")))) + .isEmpty(); assertThat(check) .executing(ctx, SkipTablesByNamePredicate.ofName(ctx, "accounts") diff --git a/pg-index-health-model/src/main/java/io/github/mfvanek/pg/model/predicates/AbstractSkipTablesPredicate.java b/pg-index-health-model/src/main/java/io/github/mfvanek/pg/model/predicates/AbstractSkipTablesPredicate.java index 147c297f..fe45b5b8 100644 --- a/pg-index-health-model/src/main/java/io/github/mfvanek/pg/model/predicates/AbstractSkipTablesPredicate.java +++ b/pg-index-health-model/src/main/java/io/github/mfvanek/pg/model/predicates/AbstractSkipTablesPredicate.java @@ -13,6 +13,7 @@ import io.github.mfvanek.pg.model.DbObject; import io.github.mfvanek.pg.model.PgContext; import io.github.mfvanek.pg.model.table.TableNameAware; +import io.github.mfvanek.pg.model.validation.Validators; import java.util.Collection; import java.util.Locale; @@ -59,12 +60,7 @@ abstract class AbstractSkipTablesPredicate implements Predicate { * @throws NullPointerException if {@code pgContext} or {@code rawTableNamesToSkip} is null */ AbstractSkipTablesPredicate(@Nonnull final PgContext pgContext, @Nonnull final Collection rawTableNamesToSkip) { - Objects.requireNonNull(pgContext, "pgContext cannot be null"); - this.fullyQualifiedTableNamesToSkip = Objects.requireNonNull(rawTableNamesToSkip, "rawTableNamesToSkip cannot be null") - .stream() - .map(pgContext::enrichWithSchema) - .map(s -> s.toLowerCase(Locale.ROOT)) - .collect(Collectors.toUnmodifiableSet()); + this.fullyQualifiedTableNamesToSkip = prepareFullyQualifiedNamesToSkip(pgContext, rawTableNamesToSkip); } /** @@ -75,7 +71,7 @@ abstract class AbstractSkipTablesPredicate implements Predicate { * Otherwise, returns {@code true}. *

* - * @param dbObject the object to be tested + * @param dbObject the database object to test; must be non-null * @return {@code false} if the {@code DbObject} matches a table name in the skip set, {@code true} otherwise */ @Override @@ -86,4 +82,38 @@ public boolean test(@Nonnull final DbObject dbObject) { } return true; } + + /** + * Prepares a set of fully qualified names to skip by enriching each raw name with schema information from the provided + * PostgreSQL context and converting it to lowercase for case-insensitive matching. + * + * @param pgContext the PostgreSQL context used to enrich each raw name with schema information; must be non-null + * @param rawNamesToSkip the collection of raw names to skip; must be non-null + * @return an unmodifiable {@link Set} of fully qualified names to skip, in lowercase + * @throws NullPointerException if {@code pgContext} or {@code rawNamesToSkip} is null + */ + @Nonnull + static Set prepareFullyQualifiedNamesToSkip(@Nonnull final PgContext pgContext, + @Nonnull final Collection rawNamesToSkip) { + Objects.requireNonNull(pgContext, "pgContext cannot be null"); + return Objects.requireNonNull(rawNamesToSkip, "rawNamesToSkip cannot be null") + .stream() + .map(pgContext::enrichWithSchema) + .map(s -> s.toLowerCase(Locale.ROOT)) + .collect(Collectors.toUnmodifiableSet()); + } + + /** + * Prepares a set containing a single name to skip, after validating that it is non-blank. + * + * @param rawNameToSkip the raw name to skip; must be non-null and non-blank + * @param argumentName the name of the argument being checked + * @return a {@link Set} containing the single validated name to skip + * @throws IllegalArgumentException if {@code rawNameToSkip} is blank + * @throws NullPointerException if {@code rawNameToSkip} is null + */ + @Nonnull + static Set prepareSingleNameToSkip(@Nonnull final String rawNameToSkip, @Nonnull final String argumentName) { + return Set.of(Validators.notBlank(rawNameToSkip, argumentName)); + } } diff --git a/pg-index-health-model/src/main/java/io/github/mfvanek/pg/model/predicates/SkipBySequenceNamePredicate.java b/pg-index-health-model/src/main/java/io/github/mfvanek/pg/model/predicates/SkipBySequenceNamePredicate.java index f1c2584e..b722498c 100644 --- a/pg-index-health-model/src/main/java/io/github/mfvanek/pg/model/predicates/SkipBySequenceNamePredicate.java +++ b/pg-index-health-model/src/main/java/io/github/mfvanek/pg/model/predicates/SkipBySequenceNamePredicate.java @@ -13,18 +13,17 @@ import io.github.mfvanek.pg.model.DbObject; import io.github.mfvanek.pg.model.PgContext; import io.github.mfvanek.pg.model.sequence.SequenceNameAware; -import io.github.mfvanek.pg.model.validation.Validators; import java.util.Collection; import java.util.Locale; -import java.util.Objects; import java.util.Set; import java.util.function.Predicate; -import java.util.stream.Collectors; import javax.annotation.Nonnull; import javax.annotation.concurrent.Immutable; import javax.annotation.concurrent.ThreadSafe; +import static io.github.mfvanek.pg.model.predicates.AbstractSkipTablesPredicate.prepareFullyQualifiedNamesToSkip; + /** * A predicate that filters out database objects based on a specified set of sequence names. * @@ -40,16 +39,11 @@ public final class SkipBySequenceNamePredicate implements Predicate { private final Set fullyQualifiedSequenceNamesToSkip; private SkipBySequenceNamePredicate(@Nonnull final PgContext pgContext, @Nonnull final Collection rawSequenceNamesToSkip) { - Objects.requireNonNull(pgContext, "pgContext cannot be null"); - this.fullyQualifiedSequenceNamesToSkip = Objects.requireNonNull(rawSequenceNamesToSkip, "rawSequenceNamesToSkip cannot be null") - .stream() - .map(pgContext::enrichWithSchema) - .map(s -> s.toLowerCase(Locale.ROOT)) - .collect(Collectors.toUnmodifiableSet()); + this.fullyQualifiedSequenceNamesToSkip = prepareFullyQualifiedNamesToSkip(pgContext, rawSequenceNamesToSkip); } private SkipBySequenceNamePredicate(@Nonnull final PgContext pgContext, @Nonnull final String rawSequenceNameToSkip) { - this(pgContext, Set.of(Validators.notBlank(rawSequenceNameToSkip, "rawSequenceNameToSkip"))); + this(pgContext, AbstractSkipTablesPredicate.prepareSingleNameToSkip(rawSequenceNameToSkip, "rawSequenceNameToSkip")); } /** diff --git a/pg-index-health-model/src/main/java/io/github/mfvanek/pg/model/predicates/SkipDbObjectsByNamePredicate.java b/pg-index-health-model/src/main/java/io/github/mfvanek/pg/model/predicates/SkipDbObjectsByNamePredicate.java index 00d89e38..591c2a26 100644 --- a/pg-index-health-model/src/main/java/io/github/mfvanek/pg/model/predicates/SkipDbObjectsByNamePredicate.java +++ b/pg-index-health-model/src/main/java/io/github/mfvanek/pg/model/predicates/SkipDbObjectsByNamePredicate.java @@ -11,7 +11,6 @@ package io.github.mfvanek.pg.model.predicates; import io.github.mfvanek.pg.model.DbObject; -import io.github.mfvanek.pg.model.validation.Validators; import java.util.Collection; import java.util.Locale; @@ -53,7 +52,7 @@ private SkipDbObjectsByNamePredicate(@Nonnull final Collection fullyQual } private SkipDbObjectsByNamePredicate(@Nonnull final String fullyQualifiedObjectNameToSkip) { - this(Set.of(Validators.notBlank(fullyQualifiedObjectNameToSkip, "fullyQualifiedObjectNameToSkip"))); + this(AbstractSkipTablesPredicate.prepareSingleNameToSkip(fullyQualifiedObjectNameToSkip, "fullyQualifiedObjectNameToSkip")); } /** diff --git a/pg-index-health-model/src/main/java/io/github/mfvanek/pg/model/predicates/SkipIndexesByNamePredicate.java b/pg-index-health-model/src/main/java/io/github/mfvanek/pg/model/predicates/SkipIndexesByNamePredicate.java new file mode 100644 index 00000000..349258bf --- /dev/null +++ b/pg-index-health-model/src/main/java/io/github/mfvanek/pg/model/predicates/SkipIndexesByNamePredicate.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2019-2024. Ivan Vakhrushev and others. + * https://github.com/mfvanek/pg-index-health + * + * This file is a part of "pg-index-health" - a Java library for + * analyzing and maintaining indexes health in PostgreSQL databases. + * + * Licensed under the Apache License 2.0 + */ + +package io.github.mfvanek.pg.model.predicates; + +import io.github.mfvanek.pg.model.DbObject; +import io.github.mfvanek.pg.model.PgContext; +import io.github.mfvanek.pg.model.index.IndexNameAware; + +import java.util.Collection; +import java.util.Locale; +import java.util.Set; +import java.util.function.Predicate; +import javax.annotation.Nonnull; +import javax.annotation.concurrent.Immutable; +import javax.annotation.concurrent.ThreadSafe; + +import static io.github.mfvanek.pg.model.predicates.AbstractSkipTablesPredicate.prepareFullyQualifiedNamesToSkip; + +/** + * A predicate that skips specified indexes by name in database objects implementing the {@link DbObject} interface. + * This class is immutable and thread-safe. + *

+ * It can be configured with either a single index name or a collection of index names to skip. The names are enriched + * with schema information, if available, to ensure they match the fully qualified index names in the database. + *

+ * + * @author Ivan Vakhrushev + * @see DbObject + * @see IndexNameAware + * @see PgContext + * @since 0.13.3 + */ +@Immutable +@ThreadSafe +public final class SkipIndexesByNamePredicate implements Predicate { + + private final Set fullyQualifiedIndexNamesToSkip; + + private SkipIndexesByNamePredicate(@Nonnull final PgContext pgContext, @Nonnull final Collection rawIndexNamesToSkip) { + this.fullyQualifiedIndexNamesToSkip = prepareFullyQualifiedNamesToSkip(pgContext, rawIndexNamesToSkip); + } + + private SkipIndexesByNamePredicate(@Nonnull final PgContext pgContext, @Nonnull final String rawIndexNameToSkip) { + this(pgContext, AbstractSkipTablesPredicate.prepareSingleNameToSkip(rawIndexNameToSkip, "rawIndexNameToSkip")); + } + + /** + * Tests whether the specified {@code DbObject} should be skipped based on its index name. + * + * @param dbObject the database object to test; must be non-null + * @return {@code true} if the {@code dbObject}'s index name does not match any of the names to skip; {@code false} otherwise + */ + @Override + public boolean test(@Nonnull final DbObject dbObject) { + if (!fullyQualifiedIndexNamesToSkip.isEmpty() && dbObject instanceof IndexNameAware) { + final IndexNameAware i = (IndexNameAware) dbObject; + return !fullyQualifiedIndexNamesToSkip.contains(i.getIndexName().toLowerCase(Locale.ROOT)); + } + return true; + } + + /** + * Creates a predicate to skip a single index name in the public schema. + * + * @param rawIndexNameToSkip the raw index name to skip; must be non-null and non-blank + * @return a {@link Predicate} to skip the specified index name + */ + public static Predicate ofName(@Nonnull final String rawIndexNameToSkip) { + return new SkipIndexesByNamePredicate(PgContext.ofPublic(), rawIndexNameToSkip); + } + + /** + * Creates a predicate to skip a collection of index names in the public schema. + * + * @param rawIndexNamesToSkip a collection of raw index names to skip; must be non-null + * @return a {@link Predicate} to skip the specified index names + */ + public static Predicate of(@Nonnull final Collection rawIndexNamesToSkip) { + return new SkipIndexesByNamePredicate(PgContext.ofPublic(), rawIndexNamesToSkip); + } + + /** + * Creates a predicate to skip a single index name in a specified schema context. + * + * @param pgContext the PostgreSQL context used to enrich the raw index name with schema information; must be non-null + * @param rawIndexNameToSkip the raw index name to skip; must be non-null and non-blank + * @return a {@link Predicate} to skip the specified index name + */ + public static Predicate ofName(@Nonnull final PgContext pgContext, @Nonnull final String rawIndexNameToSkip) { + return new SkipIndexesByNamePredicate(pgContext, rawIndexNameToSkip); + } + + /** + * Creates a predicate to skip a collection of index names in a specified schema context. + * + * @param pgContext the PostgreSQL context used to enrich each raw index name with schema information; must be non-null + * @param rawIndexNamesToSkip a collection of raw index names to skip; must be non-null + * @return a {@link Predicate} to skip the specified index names + */ + public static Predicate of(@Nonnull final PgContext pgContext, @Nonnull final Collection rawIndexNamesToSkip) { + return new SkipIndexesByNamePredicate(pgContext, rawIndexNamesToSkip); + } +} diff --git a/pg-index-health-model/src/main/java/io/github/mfvanek/pg/model/predicates/SkipTablesByNamePredicate.java b/pg-index-health-model/src/main/java/io/github/mfvanek/pg/model/predicates/SkipTablesByNamePredicate.java index 71b901bf..d6affc96 100644 --- a/pg-index-health-model/src/main/java/io/github/mfvanek/pg/model/predicates/SkipTablesByNamePredicate.java +++ b/pg-index-health-model/src/main/java/io/github/mfvanek/pg/model/predicates/SkipTablesByNamePredicate.java @@ -12,10 +12,8 @@ import io.github.mfvanek.pg.model.DbObject; import io.github.mfvanek.pg.model.PgContext; -import io.github.mfvanek.pg.model.validation.Validators; import java.util.Collection; -import java.util.Set; import java.util.function.Predicate; import javax.annotation.Nonnull; import javax.annotation.concurrent.Immutable; @@ -42,7 +40,7 @@ private SkipTablesByNamePredicate(@Nonnull final PgContext pgContext, @Nonnull f } private SkipTablesByNamePredicate(@Nonnull final PgContext pgContext, @Nonnull final String rawTableNameToSkip) { - this(pgContext, Set.of(Validators.notBlank(rawTableNameToSkip, "rawTableNameToSkip"))); + this(pgContext, prepareSingleNameToSkip(rawTableNameToSkip, "rawTableNameToSkip")); } /** diff --git a/pg-index-health-model/src/test/java/io/github/mfvanek/pg/model/predicates/AbstractSkipTablesPredicateTest.java b/pg-index-health-model/src/test/java/io/github/mfvanek/pg/model/predicates/AbstractSkipTablesPredicateTest.java index 772e176a..50f2e695 100644 --- a/pg-index-health-model/src/test/java/io/github/mfvanek/pg/model/predicates/AbstractSkipTablesPredicateTest.java +++ b/pg-index-health-model/src/test/java/io/github/mfvanek/pg/model/predicates/AbstractSkipTablesPredicateTest.java @@ -36,7 +36,7 @@ void shouldThrowExceptionWhenInvalidDataPassed() { final PgContext ctx = PgContext.ofPublic(); assertThatThrownBy(() -> new SkipTablesPredicate(ctx, null)) .isInstanceOf(NullPointerException.class) - .hasMessage("rawTableNamesToSkip cannot be null"); + .hasMessage("rawNamesToSkip cannot be null"); } @Test diff --git a/pg-index-health-model/src/test/java/io/github/mfvanek/pg/model/predicates/SkipBySequenceNamePredicateTest.java b/pg-index-health-model/src/test/java/io/github/mfvanek/pg/model/predicates/SkipBySequenceNamePredicateTest.java index 541010a8..e6c5eae6 100644 --- a/pg-index-health-model/src/test/java/io/github/mfvanek/pg/model/predicates/SkipBySequenceNamePredicateTest.java +++ b/pg-index-health-model/src/test/java/io/github/mfvanek/pg/model/predicates/SkipBySequenceNamePredicateTest.java @@ -45,7 +45,7 @@ void shouldThrowExceptionWhenInvalidDataPassed() { assertThatThrownBy(() -> SkipBySequenceNamePredicate.of(null)) .isInstanceOf(NullPointerException.class) - .hasMessage("rawSequenceNamesToSkip cannot be null"); + .hasMessage("rawNamesToSkip cannot be null"); assertThatThrownBy(() -> SkipBySequenceNamePredicate.of(null, null)) .isInstanceOf(NullPointerException.class) @@ -54,7 +54,7 @@ void shouldThrowExceptionWhenInvalidDataPassed() { final PgContext ctx = PgContext.ofPublic(); assertThatThrownBy(() -> SkipBySequenceNamePredicate.of(ctx, null)) .isInstanceOf(NullPointerException.class) - .hasMessage("rawSequenceNamesToSkip cannot be null"); + .hasMessage("rawNamesToSkip cannot be null"); assertThatThrownBy(() -> SkipBySequenceNamePredicate.ofName(ctx, null)) .isInstanceOf(NullPointerException.class) diff --git a/pg-index-health-model/src/test/java/io/github/mfvanek/pg/model/predicates/SkipIndexesByNamePredicateTest.java b/pg-index-health-model/src/test/java/io/github/mfvanek/pg/model/predicates/SkipIndexesByNamePredicateTest.java new file mode 100644 index 00000000..d6b1ca32 --- /dev/null +++ b/pg-index-health-model/src/test/java/io/github/mfvanek/pg/model/predicates/SkipIndexesByNamePredicateTest.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2019-2024. Ivan Vakhrushev and others. + * https://github.com/mfvanek/pg-index-health + * + * This file is a part of "pg-index-health" - a Java library for + * analyzing and maintaining indexes health in PostgreSQL databases. + * + * Licensed under the Apache License 2.0 + */ + +package io.github.mfvanek.pg.model.predicates; + +import io.github.mfvanek.pg.model.PgContext; +import io.github.mfvanek.pg.model.column.Column; +import io.github.mfvanek.pg.model.column.ColumnWithSerialType; +import io.github.mfvanek.pg.model.index.Index; +import io.github.mfvanek.pg.model.sequence.SequenceState; +import io.github.mfvanek.pg.model.table.Table; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.Mockito; + +import java.util.List; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class SkipIndexesByNamePredicateTest { + + @SuppressWarnings("DataFlowIssue") + @Test + void shouldThrowExceptionWhenInvalidDataPassed() { + assertThatThrownBy(() -> SkipIndexesByNamePredicate.ofName(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("rawIndexNameToSkip cannot be null"); + + assertThatThrownBy(() -> SkipIndexesByNamePredicate.ofName("")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("rawIndexNameToSkip cannot be blank"); + + assertThatThrownBy(() -> SkipIndexesByNamePredicate.ofName(" ")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("rawIndexNameToSkip cannot be blank"); + + assertThatThrownBy(() -> SkipIndexesByNamePredicate.of(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("rawNamesToSkip cannot be null"); + + assertThatThrownBy(() -> SkipIndexesByNamePredicate.of(null, null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("pgContext cannot be null"); + + final PgContext ctx = PgContext.ofPublic(); + assertThatThrownBy(() -> SkipIndexesByNamePredicate.of(ctx, null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("rawNamesToSkip cannot be null"); + + assertThatThrownBy(() -> SkipIndexesByNamePredicate.ofName(ctx, null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("rawIndexNameToSkip cannot be null"); + + assertThatThrownBy(() -> SkipIndexesByNamePredicate.ofName(ctx, "")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("rawIndexNameToSkip cannot be blank"); + + assertThatThrownBy(() -> SkipIndexesByNamePredicate.ofName(ctx, " ")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("rawIndexNameToSkip cannot be blank"); + } + + @Test + void shouldNotCastObjectsWhenExclusionsIsEmpty() { + final Index mockIndex = Mockito.mock(Index.class); + assertThat(SkipIndexesByNamePredicate.of(List.of())) + .accepts(mockIndex); + Mockito.verify(mockIndex, Mockito.never()).getIndexName(); + } + + @Test + void shouldWorkForSingleIndex() { + assertThat(SkipIndexesByNamePredicate.ofName("i2")) + .accepts(Table.of("t", 0L)) + .accepts(Index.of("t1", "i1")) + .rejects(Index.of("t2", "i2")) + .rejects(Index.of("t2", "I2")); + + final PgContext ctx = PgContext.of("CUSTOM"); + assertThat(SkipIndexesByNamePredicate.ofName(ctx, "I2")) + .accepts(Table.of("custom.t", 0L)) + .accepts(Index.of("custom.t1", "custom.i1")) + .rejects(Index.of("custom.t2", "custom.i2")) + .rejects(Index.of("custom.T2", "custom.I2")); + } + + @Test + void shouldWorkForMultipleIndexes() { + assertThat(SkipIndexesByNamePredicate.of(Set.of("i1", "I2"))) + .accepts(Table.of("t", 0L)) + .accepts(SequenceState.of("s11", "int", 80.0)) + .accepts(ColumnWithSerialType.ofSerial(Column.ofNullable("t", "c"), "s1")) + .rejects(Index.of("t1", "i1")) + .rejects(Index.of("t2", "i1")) + .rejects(Index.of("T2", "I2")); + } + + @ParameterizedTest + @ValueSource(strings = {PgContext.DEFAULT_SCHEMA_NAME, "custom"}) + void shouldWorkWithCustomSchema(final String schemaName) { + final PgContext ctx = PgContext.of(schemaName); + assertThat(SkipIndexesByNamePredicate.of(ctx, Set.of("i1", "i2"))) + .accepts(Table.of(ctx.enrichWithSchema("t"), 0L)) + .accepts(Index.of(ctx.enrichWithSchema("t1"), ctx.enrichWithSchema("i11"))) + .rejects(Index.of(ctx.enrichWithSchema("t2"), ctx.enrichWithSchema("i2"))); + } +} diff --git a/pg-index-health-model/src/test/java/io/github/mfvanek/pg/model/predicates/SkipTablesByNamePredicateTest.java b/pg-index-health-model/src/test/java/io/github/mfvanek/pg/model/predicates/SkipTablesByNamePredicateTest.java index 7840303f..ed83ea00 100644 --- a/pg-index-health-model/src/test/java/io/github/mfvanek/pg/model/predicates/SkipTablesByNamePredicateTest.java +++ b/pg-index-health-model/src/test/java/io/github/mfvanek/pg/model/predicates/SkipTablesByNamePredicateTest.java @@ -42,7 +42,7 @@ void shouldThrowExceptionWhenInvalidDataPassed() { assertThatThrownBy(() -> SkipTablesByNamePredicate.of(null)) .isInstanceOf(NullPointerException.class) - .hasMessage("rawTableNamesToSkip cannot be null"); + .hasMessage("rawNamesToSkip cannot be null"); assertThatThrownBy(() -> SkipTablesByNamePredicate.of(null, null)) .isInstanceOf(NullPointerException.class) @@ -51,7 +51,7 @@ void shouldThrowExceptionWhenInvalidDataPassed() { final PgContext ctx = PgContext.ofPublic(); assertThatThrownBy(() -> SkipTablesByNamePredicate.of(ctx, null)) .isInstanceOf(NullPointerException.class) - .hasMessage("rawTableNamesToSkip cannot be null"); + .hasMessage("rawNamesToSkip cannot be null"); assertThatThrownBy(() -> SkipTablesByNamePredicate.ofName(ctx, null)) .isInstanceOf(NullPointerException.class) diff --git a/pg-index-health/src/main/java/io/github/mfvanek/pg/checks/predicates/FilterIndexesByNamePredicate.java b/pg-index-health/src/main/java/io/github/mfvanek/pg/checks/predicates/FilterIndexesByNamePredicate.java index 3df7e874..d1ce35af 100644 --- a/pg-index-health/src/main/java/io/github/mfvanek/pg/checks/predicates/FilterIndexesByNamePredicate.java +++ b/pg-index-health/src/main/java/io/github/mfvanek/pg/checks/predicates/FilterIndexesByNamePredicate.java @@ -22,7 +22,9 @@ * * @author Ivan Vakhrushev * @since 0.6.0 + * @deprecated This class has been replaced by {@link io.github.mfvanek.pg.model.predicates.SkipIndexesByNamePredicate} */ +@Deprecated(since = "0.13.3", forRemoval = true) public class FilterIndexesByNamePredicate extends AbstractFilterByName implements Predicate { private FilterIndexesByNamePredicate(@Nonnull final Collection exclusions) { diff --git a/pg-index-health/src/test/java/io/github/mfvanek/pg/checks/cluster/IndexesWithBloatCheckOnClusterTest.java b/pg-index-health/src/test/java/io/github/mfvanek/pg/checks/cluster/IndexesWithBloatCheckOnClusterTest.java index 1126283f..bcddb1b4 100644 --- a/pg-index-health/src/test/java/io/github/mfvanek/pg/checks/cluster/IndexesWithBloatCheckOnClusterTest.java +++ b/pg-index-health/src/test/java/io/github/mfvanek/pg/checks/cluster/IndexesWithBloatCheckOnClusterTest.java @@ -11,13 +11,12 @@ package io.github.mfvanek.pg.checks.cluster; import io.github.mfvanek.pg.checks.predicates.FilterIndexesByBloatPredicate; -import io.github.mfvanek.pg.checks.predicates.FilterIndexesByNamePredicate; -import io.github.mfvanek.pg.checks.predicates.FilterIndexesBySizePredicate; import io.github.mfvanek.pg.common.maintenance.DatabaseCheckOnCluster; import io.github.mfvanek.pg.common.maintenance.Diagnostic; import io.github.mfvanek.pg.model.PgContext; -import io.github.mfvanek.pg.model.index.IndexSizeAware; import io.github.mfvanek.pg.model.index.IndexWithBloat; +import io.github.mfvanek.pg.model.predicates.SkipIndexesByNamePredicate; +import io.github.mfvanek.pg.model.predicates.SkipTablesByNamePredicate; import io.github.mfvanek.pg.support.DatabasePopulator; import io.github.mfvanek.pg.support.StatisticsAwareTestBase; import org.assertj.core.api.Assertions; @@ -25,7 +24,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; -import java.util.function.Predicate; +import java.util.List; import static io.github.mfvanek.pg.support.AbstractCheckOnClusterAssert.assertThat; @@ -74,17 +73,13 @@ void onDatabaseWithThem(final String schemaName) { .allMatch(i -> i.getIndexSizeInBytes() > 1L) .allMatch(i -> i.getBloatSizeInBytes() > 1L && i.getBloatPercentage() >= 14); - final Predicate predicate = FilterIndexesBySizePredicate.of(1L) - .and(FilterIndexesByNamePredicate.of(ctx.enrichWithSchema("accounts_pkey"))); assertThat(check) - .executing(ctx, predicate) - .hasSize(3) - .containsExactlyInAnyOrder( - IndexWithBloat.of(accountsTableName, ctx.enrichWithSchema("accounts_account_number_key"), 0L, 0L, 0), - IndexWithBloat.of(clientsTableName, ctx.enrichWithSchema("clients_pkey"), 0L, 0L, 0), - IndexWithBloat.of(clientsTableName, ctx.enrichWithSchema("i_clients_email_phone"), 0L, 0L, 0)) - .allMatch(i -> i.getIndexSizeInBytes() > 1L) - .allMatch(i -> i.getBloatSizeInBytes() > 1L && i.getBloatPercentage() >= 14); + .executing(ctx, SkipTablesByNamePredicate.of(ctx, List.of("accounts", "clients"))) + .isEmpty(); + + assertThat(check) + .executing(ctx, SkipIndexesByNamePredicate.of(ctx, List.of("accounts_account_number_key", "accounts_pkey", "clients_pkey", "i_clients_email_phone"))) + .isEmpty(); assertThat(check) .executing(ctx, FilterIndexesByBloatPredicate.of(1_000_000L, 50)) diff --git a/pg-index-health/src/test/java/io/github/mfvanek/pg/checks/cluster/IndexesWithBooleanCheckOnClusterTest.java b/pg-index-health/src/test/java/io/github/mfvanek/pg/checks/cluster/IndexesWithBooleanCheckOnClusterTest.java index d77ea13d..6c1a9fda 100644 --- a/pg-index-health/src/test/java/io/github/mfvanek/pg/checks/cluster/IndexesWithBooleanCheckOnClusterTest.java +++ b/pg-index-health/src/test/java/io/github/mfvanek/pg/checks/cluster/IndexesWithBooleanCheckOnClusterTest.java @@ -10,12 +10,13 @@ package io.github.mfvanek.pg.checks.cluster; -import io.github.mfvanek.pg.checks.predicates.FilterIndexesByNamePredicate; import io.github.mfvanek.pg.common.maintenance.DatabaseCheckOnCluster; import io.github.mfvanek.pg.common.maintenance.Diagnostic; import io.github.mfvanek.pg.model.PgContext; import io.github.mfvanek.pg.model.column.Column; import io.github.mfvanek.pg.model.index.IndexWithColumns; +import io.github.mfvanek.pg.model.predicates.SkipIndexesByNamePredicate; +import io.github.mfvanek.pg.model.predicates.SkipTablesByNamePredicate; import io.github.mfvanek.pg.support.DatabaseAwareTestBase; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -47,7 +48,11 @@ void onDatabaseWithThem(final String schemaName) { Column.ofNotNull(ctx.enrichWithSchema("accounts"), "deleted"))); assertThat(check) - .executing(ctx, FilterIndexesByNamePredicate.of(ctx.enrichWithSchema("i_accounts_deleted"))) + .executing(ctx, SkipTablesByNamePredicate.ofName(ctx, "accounts")) + .isEmpty(); + + assertThat(check) + .executing(ctx, SkipIndexesByNamePredicate.ofName(ctx, "i_accounts_deleted")) .isEmpty(); }); } diff --git a/pg-index-health/src/test/java/io/github/mfvanek/pg/checks/cluster/IndexesWithNullValuesCheckOnClusterTest.java b/pg-index-health/src/test/java/io/github/mfvanek/pg/checks/cluster/IndexesWithNullValuesCheckOnClusterTest.java index 826f4991..3ad209a9 100644 --- a/pg-index-health/src/test/java/io/github/mfvanek/pg/checks/cluster/IndexesWithNullValuesCheckOnClusterTest.java +++ b/pg-index-health/src/test/java/io/github/mfvanek/pg/checks/cluster/IndexesWithNullValuesCheckOnClusterTest.java @@ -10,11 +10,12 @@ package io.github.mfvanek.pg.checks.cluster; -import io.github.mfvanek.pg.checks.predicates.FilterIndexesByNamePredicate; import io.github.mfvanek.pg.common.maintenance.DatabaseCheckOnCluster; import io.github.mfvanek.pg.common.maintenance.Diagnostic; import io.github.mfvanek.pg.model.PgContext; import io.github.mfvanek.pg.model.index.IndexWithNulls; +import io.github.mfvanek.pg.model.predicates.SkipIndexesByNamePredicate; +import io.github.mfvanek.pg.model.predicates.SkipTablesByNamePredicate; import io.github.mfvanek.pg.support.DatabaseAwareTestBase; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -55,7 +56,11 @@ void onDatabaseWithThem(final String schemaName) { .allMatch(i -> i.getNullableColumn().isNullable()); assertThat(check) - .executing(ctx, FilterIndexesByNamePredicate.of(ctx.enrichWithSchema("i_clients_middle_name"))) + .executing(ctx, SkipTablesByNamePredicate.ofName(ctx, "clients")) + .isEmpty(); + + assertThat(check) + .executing(ctx, SkipIndexesByNamePredicate.ofName(ctx, "i_clients_middle_name")) .isEmpty(); }); } diff --git a/pg-index-health/src/test/java/io/github/mfvanek/pg/checks/cluster/InvalidIndexesCheckOnClusterTest.java b/pg-index-health/src/test/java/io/github/mfvanek/pg/checks/cluster/InvalidIndexesCheckOnClusterTest.java index 23ec408e..815ef5a2 100644 --- a/pg-index-health/src/test/java/io/github/mfvanek/pg/checks/cluster/InvalidIndexesCheckOnClusterTest.java +++ b/pg-index-health/src/test/java/io/github/mfvanek/pg/checks/cluster/InvalidIndexesCheckOnClusterTest.java @@ -10,11 +10,12 @@ package io.github.mfvanek.pg.checks.cluster; -import io.github.mfvanek.pg.checks.predicates.FilterIndexesByNamePredicate; import io.github.mfvanek.pg.common.maintenance.DatabaseCheckOnCluster; import io.github.mfvanek.pg.common.maintenance.Diagnostic; import io.github.mfvanek.pg.model.PgContext; import io.github.mfvanek.pg.model.index.Index; +import io.github.mfvanek.pg.model.predicates.SkipIndexesByNamePredicate; +import io.github.mfvanek.pg.model.predicates.SkipTablesByNamePredicate; import io.github.mfvanek.pg.support.DatabaseAwareTestBase; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -45,7 +46,11 @@ void onDatabaseWithThem(final String schemaName) { Index.of(ctx.enrichWithSchema("clients"), ctx.enrichWithSchema("i_clients_last_name_first_name"))); assertThat(check) - .executing(ctx, FilterIndexesByNamePredicate.of(ctx.enrichWithSchema("i_clients_last_name_first_name"))) + .executing(ctx, SkipTablesByNamePredicate.ofName(ctx, "clients")) + .isEmpty(); + + assertThat(check) + .executing(ctx, SkipIndexesByNamePredicate.ofName(ctx, "i_clients_last_name_first_name")) .isEmpty(); }); } diff --git a/pg-index-health/src/test/java/io/github/mfvanek/pg/checks/cluster/UnusedIndexesCheckOnClusterTest.java b/pg-index-health/src/test/java/io/github/mfvanek/pg/checks/cluster/UnusedIndexesCheckOnClusterTest.java index 54f10a5d..f6ead534 100644 --- a/pg-index-health/src/test/java/io/github/mfvanek/pg/checks/cluster/UnusedIndexesCheckOnClusterTest.java +++ b/pg-index-health/src/test/java/io/github/mfvanek/pg/checks/cluster/UnusedIndexesCheckOnClusterTest.java @@ -10,12 +10,13 @@ package io.github.mfvanek.pg.checks.cluster; -import io.github.mfvanek.pg.checks.predicates.FilterIndexesByNamePredicate; import io.github.mfvanek.pg.common.maintenance.DatabaseCheckOnCluster; import io.github.mfvanek.pg.common.maintenance.Diagnostic; import io.github.mfvanek.pg.model.PgContext; -import io.github.mfvanek.pg.model.index.IndexNameAware; import io.github.mfvanek.pg.model.index.UnusedIndex; +import io.github.mfvanek.pg.model.predicates.SkipDbObjectsByNamePredicate; +import io.github.mfvanek.pg.model.predicates.SkipIndexesByNamePredicate; +import io.github.mfvanek.pg.model.predicates.SkipTablesByNamePredicate; import io.github.mfvanek.pg.statistics.maintenance.StatisticsMaintenanceOnHost; import io.github.mfvanek.pg.support.DatabaseAwareTestBase; import io.github.mfvanek.pg.support.LogsCaptor; @@ -29,7 +30,6 @@ import java.time.OffsetDateTime; import java.util.List; import java.util.Optional; -import java.util.function.Predicate; import static io.github.mfvanek.pg.checks.cluster.UnusedIndexesCheckOnCluster.getLastStatsResetDateLogMessage; import static io.github.mfvanek.pg.checks.cluster.UnusedIndexesCheckOnCluster.getResultAsIntersection; @@ -77,18 +77,15 @@ void onDatabaseWithThem(final String schemaName) { .allMatch(i -> i.getIndexSizeInBytes() > 0L) .allMatch(i -> i.getIndexScans() == 0); - final Predicate predicate = FilterIndexesByNamePredicate.of( - List.of(ctx.enrichWithSchema("i_clients_last_first"), ctx.enrichWithSchema("i_accounts_account_number"))); assertThat(check) - .executing(ctx, predicate) - .hasSize(4) - .containsExactlyInAnyOrder( - UnusedIndex.of(ctx.enrichWithSchema("clients"), ctx.enrichWithSchema("i_clients_last_name"), 0L, 0), - UnusedIndex.of(ctx.enrichWithSchema("accounts"), ctx.enrichWithSchema("i_accounts_number_balance_not_deleted"), 0L, 0), - UnusedIndex.of(ctx.enrichWithSchema("accounts"), ctx.enrichWithSchema("i_accounts_account_number_not_deleted"), 0L, 0), - UnusedIndex.of(ctx.enrichWithSchema("accounts"), ctx.enrichWithSchema("i_accounts_id_account_number_not_deleted"), 0L, 0)) - .allMatch(i -> i.getIndexSizeInBytes() > 0L) - .allMatch(i -> i.getIndexScans() == 0); + .executing(ctx, SkipTablesByNamePredicate.ofName(ctx, "accounts") + .and(SkipIndexesByNamePredicate.of(ctx, List.of("i_clients_last_first", "i_clients_last_name")))) + .isEmpty(); + + assertThat(check) + .executing(ctx, SkipTablesByNamePredicate.ofName(ctx, "accounts") + .and(SkipDbObjectsByNamePredicate.of(List.of(ctx.enrichWithSchema("i_clients_last_first"), ctx.enrichWithSchema("i_clients_last_name"))))) + .isEmpty(); }); }