diff --git a/pg-index-health-model/src/main/java/io/github/mfvanek/pg/model/column/Column.java b/pg-index-health-model/src/main/java/io/github/mfvanek/pg/model/column/Column.java index 75e21826..e8e7116b 100644 --- a/pg-index-health-model/src/main/java/io/github/mfvanek/pg/model/column/Column.java +++ b/pg-index-health-model/src/main/java/io/github/mfvanek/pg/model/column/Column.java @@ -30,6 +30,13 @@ public class Column implements DbObject, ColumnNameAware, Comparable { private final String columnName; private final boolean notNull; + /** + * Constructs a {@code Column} object. + * + * @param tableName table name; should be non-blank. + * @param columnName column name; should be non-blank. + * @param notNull whether column is not null or nullable + */ protected Column(@Nonnull final String tableName, @Nonnull final String columnName, final boolean notNull) { @@ -126,11 +133,25 @@ public int compareTo(@Nonnull final Column other) { return Boolean.compare(notNull, other.notNull); } + /** + * Constructs a not null {@code Column} object. + * + * @param tableName table name; should be non-blank. + * @param columnName column name; should be non-blank. + * @return {@code Column} + */ @Nonnull public static Column ofNotNull(@Nonnull final String tableName, @Nonnull final String columnName) { return new Column(tableName, columnName, true); } + /** + * Constructs a nullable {@code Column} object. + * + * @param tableName table name; should be non-blank. + * @param columnName column name; should be non-blank. + * @return {@code Column} + */ @Nonnull public static Column ofNullable(@Nonnull final String tableName, @Nonnull final String columnName) { return new Column(tableName, columnName, false); diff --git a/pg-index-health-model/src/main/java/io/github/mfvanek/pg/model/column/ColumnWithSerialType.java b/pg-index-health-model/src/main/java/io/github/mfvanek/pg/model/column/ColumnWithSerialType.java index 84b6c0d3..e5a8a16e 100644 --- a/pg-index-health-model/src/main/java/io/github/mfvanek/pg/model/column/ColumnWithSerialType.java +++ b/pg-index-health-model/src/main/java/io/github/mfvanek/pg/model/column/ColumnWithSerialType.java @@ -25,7 +25,7 @@ * @since 0.6.2 */ @Immutable -public class ColumnWithSerialType implements DbObject, ColumnNameAware, Comparable { +public final class ColumnWithSerialType implements DbObject, ColumnNameAware, Comparable { private final Column column; private final SerialType serialType; @@ -44,7 +44,7 @@ private ColumnWithSerialType(@Nonnull final Column column, */ @Nonnull @Override - public final String getName() { + public String getName() { return column.getName(); } @@ -110,7 +110,7 @@ public String toString() { * {@inheritDoc} */ @Override - public final boolean equals(final Object other) { + public boolean equals(final Object other) { if (this == other) { return true; } @@ -129,7 +129,7 @@ public final boolean equals(final Object other) { * {@inheritDoc} */ @Override - public final int hashCode() { + public int hashCode() { return Objects.hash(column, serialType, sequenceName); } @@ -148,6 +148,14 @@ public int compareTo(@Nonnull final ColumnWithSerialType other) { return sequenceName.compareTo(other.sequenceName); } + /** + * Constructs a {@code ColumnWithSerialType} object of given serial type. + * + * @param column column; should be non-null. + * @param serialType column serial type; should be non-null. + * @param sequenceName sequence name; should be non-blank. + * @return {@code ColumnWithSerialType} + */ @Nonnull public static ColumnWithSerialType of(@Nonnull final Column column, @Nonnull final SerialType serialType, @@ -155,18 +163,39 @@ public static ColumnWithSerialType of(@Nonnull final Column column, return new ColumnWithSerialType(column, serialType, sequenceName); } + /** + * Constructs a {@code ColumnWithSerialType} object of {@code bigserial} type. + * + * @param column column; should be non-null. + * @param sequenceName sequence name; should be non-blank. + * @return {@code ColumnWithSerialType} + */ @Nonnull public static ColumnWithSerialType ofBigSerial(@Nonnull final Column column, @Nonnull final String sequenceName) { return of(column, SerialType.BIG_SERIAL, sequenceName); } + /** + * Constructs a {@code ColumnWithSerialType} object of {@code serial} type. + * + * @param column column; should be non-null. + * @param sequenceName sequence name; should be non-blank. + * @return {@code ColumnWithSerialType} + */ @Nonnull public static ColumnWithSerialType ofSerial(@Nonnull final Column column, @Nonnull final String sequenceName) { return of(column, SerialType.SERIAL, sequenceName); } + /** + * Constructs a {@code ColumnWithSerialType} object of {@code smallserial} type. + * + * @param column column; should be non-null. + * @param sequenceName sequence name; should be non-blank. + * @return {@code ColumnWithSerialType} + */ @Nonnull public static ColumnWithSerialType ofSmallSerial(@Nonnull final Column column, @Nonnull final String sequenceName) { diff --git a/pg-index-health-model/src/main/java/io/github/mfvanek/pg/model/index/Index.java b/pg-index-health-model/src/main/java/io/github/mfvanek/pg/model/index/Index.java index 65e29cf3..97210e78 100644 --- a/pg-index-health-model/src/main/java/io/github/mfvanek/pg/model/index/Index.java +++ b/pg-index-health-model/src/main/java/io/github/mfvanek/pg/model/index/Index.java @@ -73,7 +73,13 @@ public String toString() { return Index.class.getSimpleName() + '{' + innerToString() + '}'; } + /** + * An auxiliary utility method for implementing {@code toString()} in child classes. + * + * @return string representation of the internal fields of this class + */ @SuppressWarnings("WeakerAccess") + @Nonnull protected String innerToString() { return "tableName='" + tableName + '\'' + ", indexName='" + indexName + '\''; diff --git a/pg-index-health-model/src/main/java/io/github/mfvanek/pg/model/index/IndexWithBloat.java b/pg-index-health-model/src/main/java/io/github/mfvanek/pg/model/index/IndexWithBloat.java index e764b0f0..0e7235a6 100644 --- a/pg-index-health-model/src/main/java/io/github/mfvanek/pg/model/index/IndexWithBloat.java +++ b/pg-index-health-model/src/main/java/io/github/mfvanek/pg/model/index/IndexWithBloat.java @@ -52,6 +52,10 @@ public int getBloatPercentage() { return bloatPercentage; } + /** + * {@inheritDoc} + */ + @Nonnull @Override protected String innerToString() { return super.innerToString() + ", bloatSizeInBytes=" + bloatSizeInBytes + diff --git a/pg-index-health-model/src/main/java/io/github/mfvanek/pg/model/index/IndexWithColumns.java b/pg-index-health-model/src/main/java/io/github/mfvanek/pg/model/index/IndexWithColumns.java new file mode 100644 index 00000000..5185a812 --- /dev/null +++ b/pg-index-health-model/src/main/java/io/github/mfvanek/pg/model/index/IndexWithColumns.java @@ -0,0 +1,105 @@ +/* + * 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.index; + +import io.github.mfvanek.pg.model.column.Column; +import io.github.mfvanek.pg.model.validation.Validators; + +import java.util.List; +import java.util.Objects; +import javax.annotation.Nonnull; +import javax.annotation.concurrent.Immutable; + +/** + * Represents database index with information about size and columns. + * + * @author Ivan Vahrushev + * @since 0.10.4 + */ +@Immutable +public class IndexWithColumns extends IndexWithSize { + + private final List columns; + + @SuppressWarnings("WeakerAccess") + protected IndexWithColumns(@Nonnull final String tableName, + @Nonnull final String indexName, + final long indexSizeInBytes, + @Nonnull final List columns) { + super(tableName, indexName, indexSizeInBytes); + final List defensiveCopy = List.copyOf(Objects.requireNonNull(columns, "columns cannot be null")); + Validators.validateThatTableIsTheSame(tableName, defensiveCopy); + this.columns = defensiveCopy; + } + + /** + * Gets columns in index. + * + * @return list of columns + */ + @Nonnull + public List getColumns() { + return columns; + } + + /** + * {@inheritDoc} + */ + @Nonnull + @Override + protected String innerToString() { + return super.innerToString() + ", columns=" + columns; + } + + /** + * {@inheritDoc} + */ + @Nonnull + @Override + public String toString() { + return IndexWithColumns.class.getSimpleName() + '{' + innerToString() + '}'; + } + + /** + * Constructs an {@code IndexWithColumns} object with one column. + * + * @param tableName table name; should be non-blank. + * @param indexName index name; should be non-blank. + * @param indexSizeInBytes index size in bytes; should be positive or zero. + * @param column column in index. + * @return {@code IndexWithColumns} + */ + @Nonnull + public static IndexWithColumns ofSingle(@Nonnull final String tableName, + @Nonnull final String indexName, + final long indexSizeInBytes, + @Nonnull final Column column) { + final List columns = List.of(Objects.requireNonNull(column, "column cannot be null")); + return new IndexWithColumns(tableName, indexName, indexSizeInBytes, columns); + } + + /** + * Constructs an {@code IndexWithColumns} object with given columns. + * + * @param tableName table name; should be non-blank. + * @param indexName index name; should be non-blank. + * @param indexSizeInBytes index size in bytes; should be positive or zero. + * @param columns columns in index. + * @return {@code IndexWithColumns} + */ + @Nonnull + public static IndexWithColumns ofColumns(@Nonnull final String tableName, + @Nonnull final String indexName, + final long indexSizeInBytes, + @Nonnull final List columns) { + return new IndexWithColumns(tableName, indexName, indexSizeInBytes, columns); + } +} diff --git a/pg-index-health-model/src/main/java/io/github/mfvanek/pg/model/index/IndexWithNulls.java b/pg-index-health-model/src/main/java/io/github/mfvanek/pg/model/index/IndexWithNulls.java index 1a91255c..43d3adad 100644 --- a/pg-index-health-model/src/main/java/io/github/mfvanek/pg/model/index/IndexWithNulls.java +++ b/pg-index-health-model/src/main/java/io/github/mfvanek/pg/model/index/IndexWithNulls.java @@ -11,31 +11,36 @@ package io.github.mfvanek.pg.model.index; import io.github.mfvanek.pg.model.column.Column; -import io.github.mfvanek.pg.model.validation.Validators; import java.util.List; import java.util.Objects; import javax.annotation.Nonnull; import javax.annotation.concurrent.Immutable; +/** + * Represents database index with information about size and nullable columns. + * + * @author Ivan Vahrushev + * @since 0.0.1 + */ @Immutable -public final class IndexWithNulls extends IndexWithSize { - - private final Column nullableColumn; +public final class IndexWithNulls extends IndexWithColumns { private IndexWithNulls(@Nonnull final String tableName, @Nonnull final String indexName, final long indexSizeInBytes, @Nonnull final Column nullableColumn) { - super(tableName, indexName, indexSizeInBytes); - Objects.requireNonNull(nullableColumn, "nullableColumn cannot be null"); - Validators.validateThatTableIsTheSame(tableName, List.of(nullableColumn)); - this.nullableColumn = nullableColumn; + super(tableName, indexName, indexSizeInBytes, List.of(Objects.requireNonNull(nullableColumn, "nullableColumn cannot be null"))); } + /** + * Gets nullable column in index. + * + * @return nullable column + */ @Nonnull public Column getNullableColumn() { - return nullableColumn; + return getColumns().get(0); } /** @@ -44,11 +49,19 @@ public Column getNullableColumn() { @Nonnull @Override public String toString() { - return IndexWithNulls.class.getSimpleName() + '{' + - innerToString() + - ", nullableColumn=" + nullableColumn + '}'; + return IndexWithNulls.class.getSimpleName() + '{' + innerToString() + '}'; } + /** + * Constructs an {@code IndexWithNulls} object. + * + * @param tableName table name; should be non-blank. + * @param indexName index name; should be non-blank. + * @param indexSizeInBytes index size in bytes; should be positive or zero. + * @param nullableColumnName nullable column in this index. + * @return {@code IndexWithNulls} + */ + @Nonnull public static IndexWithNulls of(@Nonnull final String tableName, @Nonnull final String indexName, final long indexSizeInBytes, diff --git a/pg-index-health-model/src/main/java/io/github/mfvanek/pg/model/index/IndexWithSize.java b/pg-index-health-model/src/main/java/io/github/mfvanek/pg/model/index/IndexWithSize.java index b303298f..5ed2a054 100644 --- a/pg-index-health-model/src/main/java/io/github/mfvanek/pg/model/index/IndexWithSize.java +++ b/pg-index-health-model/src/main/java/io/github/mfvanek/pg/model/index/IndexWithSize.java @@ -15,6 +15,12 @@ import javax.annotation.Nonnull; import javax.annotation.concurrent.Immutable; +/** + * Represents database index with information about size. + * + * @author Ivan Vahrushev + * @since 0.0.1 + */ @Immutable public class IndexWithSize extends Index implements IndexSizeAware { @@ -36,6 +42,10 @@ public long getIndexSizeInBytes() { return indexSizeInBytes; } + /** + * {@inheritDoc} + */ + @Nonnull @Override protected String innerToString() { return super.innerToString() + ", indexSizeInBytes=" + indexSizeInBytes; @@ -50,6 +60,15 @@ public String toString() { return IndexWithSize.class.getSimpleName() + '{' + innerToString() + '}'; } + /** + * Constructs an {@code IndexWithSize} object. + * + * @param tableName table name; should be non-blank. + * @param indexName index name; should be non-blank. + * @param indexSizeInBytes index size in bytes; should be positive or zero. + * @return {@code IndexWithSize} + */ + @Nonnull public static IndexWithSize of(@Nonnull final String tableName, @Nonnull final String indexName, final long indexSizeInBytes) { diff --git a/pg-index-health-model/src/main/java/io/github/mfvanek/pg/model/table/Table.java b/pg-index-health-model/src/main/java/io/github/mfvanek/pg/model/table/Table.java index 7fe0b38b..1c3edf77 100644 --- a/pg-index-health-model/src/main/java/io/github/mfvanek/pg/model/table/Table.java +++ b/pg-index-health-model/src/main/java/io/github/mfvanek/pg/model/table/Table.java @@ -59,6 +59,12 @@ public long getTableSizeInBytes() { return tableSizeInBytes; } + /** + * An auxiliary utility method for implementing {@code toString()} in child classes. + * + * @return string representation of the internal fields of this class + */ + @Nonnull final String innerToString() { return "tableName='" + tableName + '\'' + ", tableSizeInBytes=" + tableSizeInBytes; diff --git a/pg-index-health-model/src/test/java/io/github/mfvanek/pg/model/index/IndexWithColumnsTest.java b/pg-index-health-model/src/test/java/io/github/mfvanek/pg/model/index/IndexWithColumnsTest.java new file mode 100644 index 00000000..55aaca23 --- /dev/null +++ b/pg-index-health-model/src/test/java/io/github/mfvanek/pg/model/index/IndexWithColumnsTest.java @@ -0,0 +1,131 @@ +/* + * 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.index; + +import io.github.mfvanek.pg.model.column.Column; +import nl.jqno.equalsverifier.EqualsVerifier; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class IndexWithColumnsTest { + + @Test + void gettersShouldWork() { + final Column column = Column.ofNullable("t", "f"); + final IndexWithColumns index = IndexWithColumns.ofSingle("t", "i", 11L, column); + assertThat(index.getTableName()).isEqualTo("t"); + assertThat(index.getIndexName()) + .isEqualTo("i") + .isEqualTo(index.getName()); + assertThat(index.getIndexSizeInBytes()).isEqualTo(11L); + assertThat(index.getColumns()) + .hasSize(1) + .isUnmodifiable() + .containsExactly(Column.ofNullable("t", "f")); + } + + @SuppressWarnings("ConstantConditions") + @Test + void withInvalidArguments() { + assertThatThrownBy(() -> IndexWithColumns.ofColumns(null, null, 0, null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("tableName cannot be null"); + assertThatThrownBy(() -> IndexWithColumns.ofColumns("", null, 0, null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("tableName cannot be blank"); + assertThatThrownBy(() -> IndexWithColumns.ofColumns(" ", null, 0, null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("tableName cannot be blank"); + assertThatThrownBy(() -> IndexWithColumns.ofColumns("t", null, 0, null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("indexName cannot be null"); + assertThatThrownBy(() -> IndexWithColumns.ofColumns("t", "", 0, null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("indexName cannot be blank"); + assertThatThrownBy(() -> IndexWithColumns.ofSingle("t", "i", 0, null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("column cannot be null"); + assertThatThrownBy(() -> IndexWithColumns.ofColumns("t", "i", 0, null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("columns cannot be null"); + } + + @Test + void tableShouldBeTheSame() { + final Column column = Column.ofNullable("t2", "f"); + assertThatThrownBy(() -> IndexWithColumns.ofSingle("t", "i", 1L, column)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Table name is not the same within given rows"); + } + + @Test + void testToString() { + final Column column = Column.ofNullable("t", "f"); + assertThat(IndexWithColumns.ofSingle("t", "i", 22L, column)) + .hasToString("IndexWithColumns{tableName='t', indexName='i', " + "indexSizeInBytes=22, columns=[Column{tableName='t', columnName='f', notNull=false}]}"); + } + + @SuppressWarnings("ConstantConditions") + @Test + void testEqualsAndHashCode() { + final Column column = Column.ofNullable("t1", "f"); + final IndexWithColumns first = IndexWithColumns.ofSingle("t1", "i1", 1, column); + final IndexWithColumns theSame = IndexWithColumns.ofSingle("t1", "i1", 3, column); // different size! + final IndexWithColumns second = IndexWithColumns.ofSingle("t2", "i2", 2, Column.ofNotNull("t2", "f2")); + final List columns = List.of( + Column.ofNullable("t3", "t"), + Column.ofNullable("t3", "f")); + final IndexWithColumns third = IndexWithColumns.ofColumns("t3", "i3", 2, columns); + + assertThat(first.equals(null)).isFalse(); + //noinspection EqualsBetweenInconvertibleTypes + assertThat(first.equals(Integer.MAX_VALUE)).isFalse(); + + // self + assertThat(first) + .isEqualTo(first) + .hasSameHashCodeAs(first); + + // the same + assertThat(theSame) + .isEqualTo(first) + .hasSameHashCodeAs(first); + + // others + assertThat(second) + .isNotEqualTo(first) + .doesNotHaveSameHashCodeAs(first); + + assertThat(third) + .isNotEqualTo(first) + .doesNotHaveSameHashCodeAs(first) + .isNotEqualTo(second) + .doesNotHaveSameHashCodeAs(second); + + // another + final Index anotherType = Index.of("t1", "i1"); + assertThat(anotherType) + .isEqualTo(first) + .hasSameHashCodeAs(first); + } + + @Test + @SuppressWarnings("PMD.JUnitTestsShouldIncludeAssert") + void equalsHashCodeShouldAdhereContracts() { + EqualsVerifier.forClass(IndexWithColumns.class) + .withIgnoredFields("indexSizeInBytes", "columns") + .verify(); + } +} diff --git a/pg-index-health-model/src/test/java/io/github/mfvanek/pg/model/index/IndexWithNullsTest.java b/pg-index-health-model/src/test/java/io/github/mfvanek/pg/model/index/IndexWithNullsTest.java index 70aa7ea5..602f1d26 100644 --- a/pg-index-health-model/src/test/java/io/github/mfvanek/pg/model/index/IndexWithNullsTest.java +++ b/pg-index-health-model/src/test/java/io/github/mfvanek/pg/model/index/IndexWithNullsTest.java @@ -14,9 +14,6 @@ import nl.jqno.equalsverifier.EqualsVerifier; import org.junit.jupiter.api.Test; -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; - import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -62,18 +59,10 @@ void withInvalidArguments() { .hasMessage("columnName cannot be blank"); } - @Test - void tableShouldBeTheSame() { - final Column column = Column.ofNullable("t2", "f"); - assertThatThrownBy(() -> invokePrivateConstructor("t", "i", 1L, column)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Table name is not the same within given rows"); - } - @Test void testToString() { assertThat(IndexWithNulls.of("t", "i", 22L, "f")) - .hasToString("IndexWithNulls{tableName='t', indexName='i', " + "indexSizeInBytes=22, nullableColumn=Column{tableName='t', columnName='f', notNull=false}}"); + .hasToString("IndexWithNulls{tableName='t', indexName='i', " + "indexSizeInBytes=22, columns=[Column{tableName='t', columnName='f', notNull=false}]}"); } @SuppressWarnings("ConstantConditions") @@ -120,19 +109,7 @@ void testEqualsAndHashCode() { @SuppressWarnings("PMD.JUnitTestsShouldIncludeAssert") void equalsHashCodeShouldAdhereContracts() { EqualsVerifier.forClass(IndexWithNulls.class) - .withIgnoredFields("indexSizeInBytes", "nullableColumn") + .withIgnoredFields("indexSizeInBytes", "columns") .verify(); } - - @SuppressWarnings({"unchecked", "checkstyle:IllegalThrows"}) - private static void invokePrivateConstructor(final Object... initargs) throws Throwable { - final Constructor constructor = (Constructor) IndexWithNulls.class.getDeclaredConstructors()[0]; - constructor.setAccessible(true); - - try { - constructor.newInstance(initargs); - } catch (InvocationTargetException ex) { - throw ex.getTargetException(); - } - } } diff --git a/pg-index-health/src/main/java/io/github/mfvanek/pg/checks/cluster/IndexesWithBooleanCheckOnCluster.java b/pg-index-health/src/main/java/io/github/mfvanek/pg/checks/cluster/IndexesWithBooleanCheckOnCluster.java new file mode 100644 index 00000000..bb633dff --- /dev/null +++ b/pg-index-health/src/main/java/io/github/mfvanek/pg/checks/cluster/IndexesWithBooleanCheckOnCluster.java @@ -0,0 +1,30 @@ +/* + * 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.checks.cluster; + +import io.github.mfvanek.pg.checks.host.IndexesWithBooleanCheckOnHost; +import io.github.mfvanek.pg.connection.HighAvailabilityPgConnection; +import io.github.mfvanek.pg.model.index.IndexWithColumns; + +import javax.annotation.Nonnull; + +/** + * Check for indexes that contain boolean values on all hosts in the cluster. + * + * @author Ivan Vahrushev + * @since 0.10.4 + */ +public class IndexesWithBooleanCheckOnCluster extends AbstractCheckOnCluster { + + public IndexesWithBooleanCheckOnCluster(@Nonnull final HighAvailabilityPgConnection haPgConnection) { + super(haPgConnection, IndexesWithBooleanCheckOnHost::new); + } +} diff --git a/pg-index-health/src/main/java/io/github/mfvanek/pg/checks/host/IndexesWithBooleanCheckOnHost.java b/pg-index-health/src/main/java/io/github/mfvanek/pg/checks/host/IndexesWithBooleanCheckOnHost.java new file mode 100644 index 00000000..6235cc59 --- /dev/null +++ b/pg-index-health/src/main/java/io/github/mfvanek/pg/checks/host/IndexesWithBooleanCheckOnHost.java @@ -0,0 +1,54 @@ +/* + * 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.checks.host; + +import io.github.mfvanek.pg.checks.extractors.ColumnExtractor; +import io.github.mfvanek.pg.common.maintenance.Diagnostic; +import io.github.mfvanek.pg.common.maintenance.ResultSetExtractor; +import io.github.mfvanek.pg.connection.PgConnection; +import io.github.mfvanek.pg.model.PgContext; +import io.github.mfvanek.pg.model.column.Column; +import io.github.mfvanek.pg.model.index.IndexWithColumns; + +import java.util.List; +import javax.annotation.Nonnull; + +/** + * Check for indexes that contain boolean values on a specific host. + * + * @author Ivan Vahrushev + * @since 0.10.4 + */ +public class IndexesWithBooleanCheckOnHost extends AbstractCheckOnHost { + + public IndexesWithBooleanCheckOnHost(@Nonnull final PgConnection pgConnection) { + super(IndexWithColumns.class, pgConnection, Diagnostic.INDEXES_WITH_BOOLEAN); + } + + /** + * Returns indexes that contain boolean values in the specified schema. + * + * @param pgContext check's context with the specified schema + * @return list of indexes that contain boolean values + */ + @Nonnull + @Override + public List check(@Nonnull final PgContext pgContext) { + final ResultSetExtractor columnExtractor = ColumnExtractor.of(); + return executeQuery(pgContext, rs -> { + final String tableName = rs.getString(TABLE_NAME); + final String indexName = rs.getString(INDEX_NAME); + final long indexSize = rs.getLong(INDEX_SIZE); + final Column column = columnExtractor.extractData(rs); + return IndexWithColumns.ofSingle(tableName, indexName, indexSize, column); + }); + } +} diff --git a/pg-index-health/src/main/java/io/github/mfvanek/pg/common/health/logger/AbstractHealthLogger.java b/pg-index-health/src/main/java/io/github/mfvanek/pg/common/health/logger/AbstractHealthLogger.java index f81062af..aadf204d 100644 --- a/pg-index-health/src/main/java/io/github/mfvanek/pg/common/health/logger/AbstractHealthLogger.java +++ b/pg-index-health/src/main/java/io/github/mfvanek/pg/common/health/logger/AbstractHealthLogger.java @@ -32,6 +32,7 @@ import io.github.mfvanek.pg.model.index.DuplicatedIndexes; import io.github.mfvanek.pg.model.index.Index; import io.github.mfvanek.pg.model.index.IndexWithBloat; +import io.github.mfvanek.pg.model.index.IndexWithColumns; import io.github.mfvanek.pg.model.index.IndexWithNulls; import io.github.mfvanek.pg.model.index.UnusedIndex; import io.github.mfvanek.pg.model.table.Table; @@ -94,6 +95,7 @@ public final List logAll(@Nonnull final Exclusions exclusions, logResult.add(logColumnsWithJsonType(databaseChecks, pgContext)); logResult.add(logColumnsWithSerialTypes(databaseChecks, pgContext)); logResult.add(logFunctionsWithoutDescription(databaseChecks, pgContext)); + logResult.add(logIndexesWithBoolean(databaseChecks, pgContext)); return logResult; } @@ -222,6 +224,13 @@ private String logFunctionsWithoutDescription(@Nonnull final DatabaseChecks data c -> true, pgContext, SimpleLoggingKey.FUNCTIONS_WITHOUT_DESCRIPTION); } + @Nonnull + private String logIndexesWithBoolean(@Nonnull final DatabaseChecks databaseChecks, + @Nonnull final PgContext pgContext) { + return logCheckResult(databaseChecks.getCheck(Diagnostic.INDEXES_WITH_BOOLEAN, IndexWithColumns.class), + c -> true, pgContext, SimpleLoggingKey.INDEXES_WITH_BOOLEAN); + } + @Nonnull private String logCheckResult(@Nonnull final DatabaseCheckOnCluster check, @Nonnull final Predicate exclusionsFilter, diff --git a/pg-index-health/src/main/java/io/github/mfvanek/pg/common/health/logger/SimpleLoggingKey.java b/pg-index-health/src/main/java/io/github/mfvanek/pg/common/health/logger/SimpleLoggingKey.java index 54a78667..61bf169a 100644 --- a/pg-index-health/src/main/java/io/github/mfvanek/pg/common/health/logger/SimpleLoggingKey.java +++ b/pg-index-health/src/main/java/io/github/mfvanek/pg/common/health/logger/SimpleLoggingKey.java @@ -29,7 +29,8 @@ public enum SimpleLoggingKey implements LoggingKey { COLUMNS_WITHOUT_DESCRIPTION("columns_without_description"), COLUMNS_WITH_JSON_TYPE("columns_with_json_type"), COLUMNS_WITH_SERIAL_TYPES("columns_with_serial_types"), - FUNCTIONS_WITHOUT_DESCRIPTION("functions_without_description"); + FUNCTIONS_WITHOUT_DESCRIPTION("functions_without_description"), + INDEXES_WITH_BOOLEAN("indexes_with_boolean"); private final String subKeyName; diff --git a/pg-index-health/src/main/java/io/github/mfvanek/pg/common/maintenance/DatabaseChecks.java b/pg-index-health/src/main/java/io/github/mfvanek/pg/common/maintenance/DatabaseChecks.java index 5a9bc699..9a79f54e 100644 --- a/pg-index-health/src/main/java/io/github/mfvanek/pg/common/maintenance/DatabaseChecks.java +++ b/pg-index-health/src/main/java/io/github/mfvanek/pg/common/maintenance/DatabaseChecks.java @@ -17,6 +17,7 @@ import io.github.mfvanek.pg.checks.cluster.ForeignKeysNotCoveredWithIndexCheckOnCluster; import io.github.mfvanek.pg.checks.cluster.FunctionsWithoutDescriptionCheckOnCluster; import io.github.mfvanek.pg.checks.cluster.IndexesWithBloatCheckOnCluster; +import io.github.mfvanek.pg.checks.cluster.IndexesWithBooleanCheckOnCluster; import io.github.mfvanek.pg.checks.cluster.IndexesWithNullValuesCheckOnCluster; import io.github.mfvanek.pg.checks.cluster.IntersectedIndexesCheckOnCluster; import io.github.mfvanek.pg.checks.cluster.InvalidIndexesCheckOnCluster; @@ -56,7 +57,8 @@ public DatabaseChecks(@Nonnull final HighAvailabilityPgConnection haPgConnection new ColumnsWithoutDescriptionCheckOnCluster(haPgConnection), new ColumnsWithJsonTypeCheckOnCluster(haPgConnection), new ColumnsWithSerialTypesCheckOnCluster(haPgConnection), - new FunctionsWithoutDescriptionCheckOnCluster(haPgConnection)); + new FunctionsWithoutDescriptionCheckOnCluster(haPgConnection), + new IndexesWithBooleanCheckOnCluster(haPgConnection)); allChecks.forEach(check -> this.checks.putIfAbsent(check.getDiagnostic(), check)); } diff --git a/pg-index-health/src/main/java/io/github/mfvanek/pg/common/maintenance/Diagnostic.java b/pg-index-health/src/main/java/io/github/mfvanek/pg/common/maintenance/Diagnostic.java index 1007c484..61f8eeba 100644 --- a/pg-index-health/src/main/java/io/github/mfvanek/pg/common/maintenance/Diagnostic.java +++ b/pg-index-health/src/main/java/io/github/mfvanek/pg/common/maintenance/Diagnostic.java @@ -38,12 +38,20 @@ public enum Diagnostic { COLUMNS_WITHOUT_DESCRIPTION(ExecutionTopology.ON_PRIMARY, "columns_without_description.sql", QueryExecutors::executeQueryWithSchema), COLUMNS_WITH_JSON_TYPE(ExecutionTopology.ON_PRIMARY, "columns_with_json_type.sql", QueryExecutors::executeQueryWithSchema), COLUMNS_WITH_SERIAL_TYPES(ExecutionTopology.ON_PRIMARY, "non_primary_key_columns_with_serial_types.sql", QueryExecutors::executeQueryWithSchema), - FUNCTIONS_WITHOUT_DESCRIPTION(ExecutionTopology.ON_PRIMARY, "functions_without_description.sql", QueryExecutors::executeQueryWithSchema); + FUNCTIONS_WITHOUT_DESCRIPTION(ExecutionTopology.ON_PRIMARY, "functions_without_description.sql", QueryExecutors::executeQueryWithSchema), + INDEXES_WITH_BOOLEAN(ExecutionTopology.ON_PRIMARY, "indexes_with_boolean.sql", QueryExecutors::executeQueryWithSchema); private final ExecutionTopology executionTopology; private final String sqlQueryFileName; private final QueryExecutor queryExecutor; + /** + * Creates a {@code Diagnostic} instance. + * + * @param executionTopology the place where the diagnostic should be executed + * @param sqlQueryFileName the associated sql query file name + * @param queryExecutor the lambda which executes the associated sql query + */ Diagnostic(@Nonnull final ExecutionTopology executionTopology, @Nonnull final String sqlQueryFileName, @Nonnull final QueryExecutor queryExecutor) { @@ -52,21 +60,41 @@ public enum Diagnostic { this.queryExecutor = Objects.requireNonNull(queryExecutor, "queryExecutor cannot be null"); } + /** + * Gets the place where the diagnostic should be executed. + * + * @return {@code ExecutionTopology} + */ @Nonnull public ExecutionTopology getExecutionTopology() { return executionTopology; } + /** + * Gets the associated sql query file name. + * + * @return sql query file name + */ @Nonnull public String getSqlQueryFileName() { return sqlQueryFileName; } + /** + * Gets the lambda which executes the associated sql query. + * + * @return {@code QueryExecutor} + */ @Nonnull public QueryExecutor getQueryExecutor() { return queryExecutor; } + /** + * Shows whether diagnostic results should be collected from all nodes in the cluster. + * + * @return true if diagnostic results should be collected from all nodes in the cluster + */ public boolean isAcrossCluster() { return executionTopology == ExecutionTopology.ACROSS_CLUSTER; } diff --git a/pg-index-health/src/main/resources b/pg-index-health/src/main/resources index 8739c65d..3ac37cc8 160000 --- a/pg-index-health/src/main/resources +++ b/pg-index-health/src/main/resources @@ -1 +1 @@ -Subproject commit 8739c65d9cfe542c71377e776eae176f0bcc667f +Subproject commit 3ac37cc82fc72da1de565555a487f0f66c3387d6 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 new file mode 100644 index 00000000..86e6d550 --- /dev/null +++ b/pg-index-health/src/test/java/io/github/mfvanek/pg/checks/cluster/IndexesWithBooleanCheckOnClusterTest.java @@ -0,0 +1,50 @@ +/* + * 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.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.support.DatabaseAwareTestBase; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.assertThat; + +class IndexesWithBooleanCheckOnClusterTest extends DatabaseAwareTestBase { + + private final DatabaseCheckOnCluster check = new IndexesWithBooleanCheckOnCluster(getHaPgConnection()); + + @Test + void shouldSatisfyContract() { + assertThat(check.getType()).isEqualTo(IndexWithColumns.class); + assertThat(check.getDiagnostic()).isEqualTo(Diagnostic.INDEXES_WITH_BOOLEAN); + } + + @ParameterizedTest + @ValueSource(strings = {PgContext.DEFAULT_SCHEMA_NAME, "custom"}) + void onDatabaseWithThem(final String schemaName) { + executeTestOnDatabase(schemaName, dbp -> dbp.withReferences().withBooleanValuesInIndex(), ctx -> { + assertThat(check.check(ctx)) + .hasSize(1) + .containsExactly( + IndexWithColumns.ofSingle(ctx.enrichWithSchema("accounts"), ctx.enrichWithSchema("i_accounts_deleted"), 0L, + Column.ofNotNull(ctx.enrichWithSchema("accounts"), "deleted"))); + + assertThat(check.check(ctx, FilterIndexesByNamePredicate.of(ctx.enrichWithSchema("i_accounts_deleted")))) + .isEmpty(); + }); + } +} diff --git a/pg-index-health/src/test/java/io/github/mfvanek/pg/checks/host/IndexesWithBooleanCheckOnHostTest.java b/pg-index-health/src/test/java/io/github/mfvanek/pg/checks/host/IndexesWithBooleanCheckOnHostTest.java new file mode 100644 index 00000000..bcdb138e --- /dev/null +++ b/pg-index-health/src/test/java/io/github/mfvanek/pg/checks/host/IndexesWithBooleanCheckOnHostTest.java @@ -0,0 +1,49 @@ +/* + * 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.checks.host; + +import io.github.mfvanek.pg.common.maintenance.DatabaseCheckOnHost; +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.support.DatabaseAwareTestBase; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static io.github.mfvanek.pg.support.AbstractCheckOnHostAssert.assertThat; + +class IndexesWithBooleanCheckOnHostTest extends DatabaseAwareTestBase { + + private final DatabaseCheckOnHost check = new IndexesWithBooleanCheckOnHost(getPgConnection()); + + @Test + void shouldSatisfyContract() { + assertThat(check) + .hasType(IndexWithColumns.class) + .hasDiagnostic(Diagnostic.INDEXES_WITH_BOOLEAN) + .hasHost(getHost()); + } + + @ParameterizedTest + @ValueSource(strings = {PgContext.DEFAULT_SCHEMA_NAME, "custom"}) + void onDatabaseWithThem(final String schemaName) { + executeTestOnDatabase(schemaName, dbp -> dbp.withReferences().withBooleanValuesInIndex(), ctx -> + assertThat(check) + .executing(ctx) + .hasSize(1) + .containsExactly( + IndexWithColumns.ofSingle(ctx.enrichWithSchema("accounts"), ctx.enrichWithSchema("i_accounts_deleted"), 0L, + Column.ofNotNull(ctx.enrichWithSchema("accounts"), "deleted")) + )); + } +} diff --git a/pg-index-health/src/test/java/io/github/mfvanek/pg/common/health/logger/HealthLoggerTest.java b/pg-index-health/src/test/java/io/github/mfvanek/pg/common/health/logger/HealthLoggerTest.java index 883c9e20..86aad017 100644 --- a/pg-index-health/src/test/java/io/github/mfvanek/pg/common/health/logger/HealthLoggerTest.java +++ b/pg-index-health/src/test/java/io/github/mfvanek/pg/common/health/logger/HealthLoggerTest.java @@ -62,6 +62,7 @@ void logAll(final String schemaName) { .withData() .withInvalidIndex() .withNullValuesInIndex() + .withBooleanValuesInIndex() .withTableWithoutPrimaryKey() .withDuplicatedIndex() .withNonSuitableIndex() @@ -78,16 +79,17 @@ void logAll(final String schemaName) { "1999-12-31T23:59:59Z\tdb_indexes_health\tforeign_keys_without_index\t1", "1999-12-31T23:59:59Z\tdb_indexes_health\ttables_without_primary_key\t1", "1999-12-31T23:59:59Z\tdb_indexes_health\tindexes_with_null_values\t1", - "1999-12-31T23:59:59Z\tdb_indexes_health\tindexes_with_bloat\t11", + "1999-12-31T23:59:59Z\tdb_indexes_health\tindexes_with_bloat\t13", "1999-12-31T23:59:59Z\tdb_indexes_health\ttables_with_bloat\t2", - "1999-12-31T23:59:59Z\tdb_indexes_health\tintersected_indexes\t5", - "1999-12-31T23:59:59Z\tdb_indexes_health\tunused_indexes\t7", + "1999-12-31T23:59:59Z\tdb_indexes_health\tintersected_indexes\t7", + "1999-12-31T23:59:59Z\tdb_indexes_health\tunused_indexes\t8", "1999-12-31T23:59:59Z\tdb_indexes_health\ttables_with_missing_indexes\t0", "1999-12-31T23:59:59Z\tdb_indexes_health\ttables_without_description\t4", "1999-12-31T23:59:59Z\tdb_indexes_health\tcolumns_without_description\t17", "1999-12-31T23:59:59Z\tdb_indexes_health\tcolumns_with_json_type\t1", "1999-12-31T23:59:59Z\tdb_indexes_health\tcolumns_with_serial_types\t2", - "1999-12-31T23:59:59Z\tdb_indexes_health\tfunctions_without_description\t2"); + "1999-12-31T23:59:59Z\tdb_indexes_health\tfunctions_without_description\t2", + "1999-12-31T23:59:59Z\tdb_indexes_health\tindexes_with_boolean\t1"); }); } diff --git a/pg-index-health/src/test/java/io/github/mfvanek/pg/common/health/logger/StandardHealthLoggerTest.java b/pg-index-health/src/test/java/io/github/mfvanek/pg/common/health/logger/StandardHealthLoggerTest.java index e9a29bd8..3b85cc80 100644 --- a/pg-index-health/src/test/java/io/github/mfvanek/pg/common/health/logger/StandardHealthLoggerTest.java +++ b/pg-index-health/src/test/java/io/github/mfvanek/pg/common/health/logger/StandardHealthLoggerTest.java @@ -39,6 +39,7 @@ void logAll(final String schemaName) { .withData() .withInvalidIndex() .withNullValuesInIndex() + .withBooleanValuesInIndex() .withTableWithoutPrimaryKey() .withDuplicatedIndex() .withNonSuitableIndex() @@ -56,16 +57,17 @@ void logAll(final String schemaName) { "foreign_keys_without_index:1", "tables_without_primary_key:1", "indexes_with_null_values:1", - "indexes_with_bloat:11", + "indexes_with_bloat:13", "tables_with_bloat:2", - "intersected_indexes:5", - "unused_indexes:7", + "intersected_indexes:7", + "unused_indexes:8", "tables_with_missing_indexes:0", "tables_without_description:4", "columns_without_description:17", "columns_with_json_type:1", "columns_with_serial_types:2", - "functions_without_description:2"); + "functions_without_description:2", + "indexes_with_boolean:1"); }); } diff --git a/pg-index-health/src/test/java/io/github/mfvanek/pg/support/DatabasePopulator.java b/pg-index-health/src/test/java/io/github/mfvanek/pg/support/DatabasePopulator.java index 46f59236..cea357b1 100644 --- a/pg-index-health/src/test/java/io/github/mfvanek/pg/support/DatabasePopulator.java +++ b/pg-index-health/src/test/java/io/github/mfvanek/pg/support/DatabasePopulator.java @@ -28,6 +28,7 @@ import io.github.mfvanek.pg.support.statements.CreateDuplicatedIndexStatement; import io.github.mfvanek.pg.support.statements.CreateForeignKeyOnNullableColumnStatement; import io.github.mfvanek.pg.support.statements.CreateFunctionsStatement; +import io.github.mfvanek.pg.support.statements.CreateIndexWithBooleanValues; import io.github.mfvanek.pg.support.statements.CreateIndexWithNullValues; import io.github.mfvanek.pg.support.statements.CreateIndexesWithDifferentOpclassStatement; import io.github.mfvanek.pg.support.statements.CreateMaterializedViewStatement; @@ -135,6 +136,12 @@ public DatabasePopulator withNullValuesInIndex() { return this; } + @Nonnull + public DatabasePopulator withBooleanValuesInIndex() { + statementsToExecuteInSameTransaction.putIfAbsent(49, new CreateIndexWithBooleanValues(schemaName)); + return this; + } + @Nonnull public DatabasePopulator withDifferentOpclassIndexes() { statementsToExecuteInSameTransaction.putIfAbsent(50, new CreateIndexesWithDifferentOpclassStatement(schemaName)); diff --git a/pg-index-health/src/test/java/io/github/mfvanek/pg/support/statements/CreateIndexWithBooleanValues.java b/pg-index-health/src/test/java/io/github/mfvanek/pg/support/statements/CreateIndexWithBooleanValues.java new file mode 100644 index 00000000..bb3efd69 --- /dev/null +++ b/pg-index-health/src/test/java/io/github/mfvanek/pg/support/statements/CreateIndexWithBooleanValues.java @@ -0,0 +1,30 @@ +/* + * 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.support.statements; + +import java.sql.SQLException; +import java.sql.Statement; +import javax.annotation.Nonnull; + +public class CreateIndexWithBooleanValues extends AbstractDbStatement { + + public CreateIndexWithBooleanValues(@Nonnull final String schemaName) { + super(schemaName); + } + + @Override + public void execute(@Nonnull final Statement statement) throws SQLException { + statement.execute(String.format("create index if not exists i_accounts_deleted " + + "on %s.accounts (deleted)", schemaName)); + statement.execute(String.format("create unique index if not exists i_accounts_account_number_deleted " + + "on %s.accounts (account_number, deleted)", schemaName)); + } +}