Skip to content

Commit

Permalink
Add SkipIndexesByNamePredicate (#490)
Browse files Browse the repository at this point in the history
* Add SkipIndexesByNamePredicate

* Refactor tests
  • Loading branch information
mfvanek authored Nov 11, 2024
1 parent 13e3d82 commit eba2fc6
Show file tree
Hide file tree
Showing 20 changed files with 343 additions and 63 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -59,12 +60,7 @@ abstract class AbstractSkipTablesPredicate implements Predicate<DbObject> {
* @throws NullPointerException if {@code pgContext} or {@code rawTableNamesToSkip} is null
*/
AbstractSkipTablesPredicate(@Nonnull final PgContext pgContext, @Nonnull final Collection<String> 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);
}

/**
Expand All @@ -75,7 +71,7 @@ abstract class AbstractSkipTablesPredicate implements Predicate<DbObject> {
* Otherwise, returns {@code true}.
* </p>
*
* @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
Expand All @@ -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<String> prepareFullyQualifiedNamesToSkip(@Nonnull final PgContext pgContext,
@Nonnull final Collection<String> 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<String> prepareSingleNameToSkip(@Nonnull final String rawNameToSkip, @Nonnull final String argumentName) {
return Set.of(Validators.notBlank(rawNameToSkip, argumentName));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -40,16 +39,11 @@ public final class SkipBySequenceNamePredicate implements Predicate<DbObject> {
private final Set<String> fullyQualifiedSequenceNamesToSkip;

private SkipBySequenceNamePredicate(@Nonnull final PgContext pgContext, @Nonnull final Collection<String> 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"));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -53,7 +52,7 @@ private SkipDbObjectsByNamePredicate(@Nonnull final Collection<String> fullyQual
}

private SkipDbObjectsByNamePredicate(@Nonnull final String fullyQualifiedObjectNameToSkip) {
this(Set.of(Validators.notBlank(fullyQualifiedObjectNameToSkip, "fullyQualifiedObjectNameToSkip")));
this(AbstractSkipTablesPredicate.prepareSingleNameToSkip(fullyQualifiedObjectNameToSkip, "fullyQualifiedObjectNameToSkip"));
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* 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.
* </p>
*
* @author Ivan Vakhrushev
* @see DbObject
* @see IndexNameAware
* @see PgContext
* @since 0.13.3
*/
@Immutable
@ThreadSafe
public final class SkipIndexesByNamePredicate implements Predicate<DbObject> {

private final Set<String> fullyQualifiedIndexNamesToSkip;

private SkipIndexesByNamePredicate(@Nonnull final PgContext pgContext, @Nonnull final Collection<String> 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<DbObject> 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<DbObject> of(@Nonnull final Collection<String> 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<DbObject> 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<DbObject> of(@Nonnull final PgContext pgContext, @Nonnull final Collection<String> rawIndexNamesToSkip) {
return new SkipIndexesByNamePredicate(pgContext, rawIndexNamesToSkip);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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"));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down
Loading

0 comments on commit eba2fc6

Please sign in to comment.