From 2a35da2ace135269ce52a0a1270901e9f5bea126 Mon Sep 17 00:00:00 2001 From: Ivan Vakhrushev Date: Sat, 12 Oct 2024 14:06:32 +0400 Subject: [PATCH] Add checks for duplicated and intersected foreign keys (#451) * Add DuplicatedForeignKeysCheckOnHost * Add test for DuplicatedForeignKeysCheckOnHost * Add IntersectedForeignKeysCheckOnHost * Add tests for IntersectedForeignKeysCheckOnHost * Add checks on cluster * Fix error in tests * Add code to HealthLogger * Add new beans to the starter --- README.md | 6 +- .../pg/checks/extractors/ColumnExtractor.java | 5 + .../ColumnWithSerialTypeExtractor.java | 5 + .../DuplicatedForeignKeysExtractor.java | 59 ++++++++++++ .../extractors/ForeignKeyExtractor.java | 93 +++++++++++++++++++ .../IndexWithSingleColumnExtractor.java | 5 + .../pg/checks/extractors/TableExtractor.java | 5 + .../pg/checks/host/AbstractCheckOnHost.java | 1 - .../DuplicatedForeignKeysCheckOnHost.java | 50 ++++++++++ ...ignKeysNotCoveredWithIndexCheckOnHost.java | 13 +-- .../IntersectedForeignKeysCheckOnHost.java | 52 +++++++++++ .../host/NotValidConstraintsCheckOnHost.java | 2 + .../pg/common/maintenance/Diagnostic.java | 4 +- .../DuplicatedForeignKeysCheckOnHostTest.java | 55 +++++++++++ .../host/IndexesWithBloatCheckOnHostTest.java | 11 ++- ...IntersectedForeignKeysCheckOnHostTest.java | 66 +++++++++++++ .../mfvanek/pg/support/DatabasePopulator.java | 14 +++ .../AddDuplicatedForeignKeysStatement.java | 25 +++++ .../AddIntersectedForeignKeysStatement.java | 34 +++++++ .../CreateClientsTableStatement.java | 7 +- .../CreateTableWithIdentityPrimaryKey.java | 2 +- .../InsertDataIntoTablesAction.java | 12 ++- .../health/logger/AbstractHealthLogger.java | 19 +++- .../health/logger/SimpleLoggingKey.java | 4 +- .../pg/common/maintenance/DatabaseChecks.java | 6 +- .../health/logger/HealthLoggerTest.java | 18 ++-- .../logger/StandardHealthLoggerTest.java | 18 ++-- .../constraint/DuplicatedForeignKeys.java | 1 + .../DuplicatedForeignKeysCheckOnCluster.java | 30 ++++++ .../IntersectedForeignKeysCheckOnCluster.java | 30 ++++++ ...plicatedForeignKeysCheckOnClusterTest.java | 56 +++++++++++ .../IndexesWithBloatCheckOnClusterTest.java | 19 ++-- ...ersectedForeignKeysCheckOnClusterTest.java | 66 +++++++++++++ .../kt/PostgresDemoApplicationKtTest.kt | 13 +++ ...abaseStructureHealthAutoConfiguration.java | 18 ++++ .../pg/spring/AutoConfigurationTestBase.java | 4 +- ...eHealthAutoConfigurationFilteringTest.java | 6 +- .../postgres/PostgresDemoApplicationTest.java | 15 +++ 38 files changed, 799 insertions(+), 50 deletions(-) create mode 100644 pg-index-health-core/src/main/java/io/github/mfvanek/pg/checks/extractors/DuplicatedForeignKeysExtractor.java create mode 100644 pg-index-health-core/src/main/java/io/github/mfvanek/pg/checks/extractors/ForeignKeyExtractor.java create mode 100644 pg-index-health-core/src/main/java/io/github/mfvanek/pg/checks/host/DuplicatedForeignKeysCheckOnHost.java create mode 100644 pg-index-health-core/src/main/java/io/github/mfvanek/pg/checks/host/IntersectedForeignKeysCheckOnHost.java create mode 100644 pg-index-health-core/src/test/java/io/github/mfvanek/pg/checks/host/DuplicatedForeignKeysCheckOnHostTest.java create mode 100644 pg-index-health-core/src/test/java/io/github/mfvanek/pg/checks/host/IntersectedForeignKeysCheckOnHostTest.java create mode 100644 pg-index-health-core/src/testFixtures/java/io/github/mfvanek/pg/support/statements/AddDuplicatedForeignKeysStatement.java create mode 100644 pg-index-health-core/src/testFixtures/java/io/github/mfvanek/pg/support/statements/AddIntersectedForeignKeysStatement.java create mode 100644 pg-index-health/src/main/java/io/github/mfvanek/pg/checks/cluster/DuplicatedForeignKeysCheckOnCluster.java create mode 100644 pg-index-health/src/main/java/io/github/mfvanek/pg/checks/cluster/IntersectedForeignKeysCheckOnCluster.java create mode 100644 pg-index-health/src/test/java/io/github/mfvanek/pg/checks/cluster/DuplicatedForeignKeysCheckOnClusterTest.java create mode 100644 pg-index-health/src/test/java/io/github/mfvanek/pg/checks/cluster/IntersectedForeignKeysCheckOnClusterTest.java diff --git a/README.md b/README.md index 2f3a826c..c5d02025 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,9 @@ All checks can be divided into 2 groups: | 17 | Tables with [not valid constraints](https://habr.com/ru/articles/800121/) | **runtime**/static | [sql](https://github.com/mfvanek/pg-index-health-sql/blob/master/sql/check_not_valid_constraints.sql) | | 18 | B-tree indexes [on array columns](https://habr.com/ru/articles/800121/) | static | [sql](https://github.com/mfvanek/pg-index-health-sql/blob/master/sql/btree_indexes_on_array_columns.sql) | | 19 | [Sequence overflow](https://habr.com/ru/articles/800121/) | **runtime** | [sql](https://github.com/mfvanek/pg-index-health-sql/blob/master/sql/sequence_overflow.sql) | -| 20 | Primary keys with [serial types](https://wiki.postgresql.org/wiki/Don't_Do_This#Don.27t_use_serial) | static | [sql](https://github.com/mfvanek/pg-index-health-sql/blob/master/sql/primary_keys_with_serial_types.sql) | | +| 20 | Primary keys with [serial types](https://wiki.postgresql.org/wiki/Don't_Do_This#Don.27t_use_serial) | static | [sql](https://github.com/mfvanek/pg-index-health-sql/blob/master/sql/primary_keys_with_serial_types.sql) | +| 21 | Duplicated ([completely identical](https://habr.com/ru/articles/803841/)) foreign keys | static | [sql](https://github.com/mfvanek/pg-index-health-sql/blob/master/sql/duplicated_foreign_keys.sql) | +| 22 | Intersected ([partially identical](https://habr.com/ru/articles/803841/)) foreign keys | static | [sql](https://github.com/mfvanek/pg-index-health-sql/blob/master/sql/intersected_foreign_keys.sql) | For raw sql queries see [pg-index-health-sql](https://github.com/mfvanek/pg-index-health-sql) project. @@ -126,7 +128,7 @@ All these cases are covered with examples in the [pg-index-health-demo](https:// There is a Spring Boot starter [pg-index-health-test-starter](spring-boot-integration%2Fpg-index-health-test-starter) for unit/integration testing as well. -More examples you can find in [pg-index-health-spring-boot-demo](https://github.com/mfvanek/pg-index-health-spring-boot-demo) project. +More examples you can find in [pg-index-health-demo](https://github.com/mfvanek/pg-index-health-demo) project. ### Starter installation diff --git a/pg-index-health-core/src/main/java/io/github/mfvanek/pg/checks/extractors/ColumnExtractor.java b/pg-index-health-core/src/main/java/io/github/mfvanek/pg/checks/extractors/ColumnExtractor.java index 1bd03119..0656a941 100644 --- a/pg-index-health-core/src/main/java/io/github/mfvanek/pg/checks/extractors/ColumnExtractor.java +++ b/pg-index-health-core/src/main/java/io/github/mfvanek/pg/checks/extractors/ColumnExtractor.java @@ -45,6 +45,11 @@ public Column extractData(@Nonnull final ResultSet resultSet) throws SQLExceptio return Column.ofNullable(tableName, columnName); } + /** + * Creates {@code ColumnExtractor} instance. + * + * @return {@code ColumnExtractor} instance + */ @Nonnull public static ResultSetExtractor of() { return new ColumnExtractor(); diff --git a/pg-index-health-core/src/main/java/io/github/mfvanek/pg/checks/extractors/ColumnWithSerialTypeExtractor.java b/pg-index-health-core/src/main/java/io/github/mfvanek/pg/checks/extractors/ColumnWithSerialTypeExtractor.java index ea828792..1a75a7c8 100644 --- a/pg-index-health-core/src/main/java/io/github/mfvanek/pg/checks/extractors/ColumnWithSerialTypeExtractor.java +++ b/pg-index-health-core/src/main/java/io/github/mfvanek/pg/checks/extractors/ColumnWithSerialTypeExtractor.java @@ -39,6 +39,11 @@ public ColumnWithSerialType extractData(@Nonnull final ResultSet resultSet) thro return ColumnWithSerialType.of(column, SerialType.valueFrom(columnType), sequenceName); } + /** + * Creates {@code ColumnWithSerialTypeExtractor} instance. + * + * @return {@code ColumnWithSerialTypeExtractor} instance + */ @Nonnull public static ResultSetExtractor of() { return new ColumnWithSerialTypeExtractor(); diff --git a/pg-index-health-core/src/main/java/io/github/mfvanek/pg/checks/extractors/DuplicatedForeignKeysExtractor.java b/pg-index-health-core/src/main/java/io/github/mfvanek/pg/checks/extractors/DuplicatedForeignKeysExtractor.java new file mode 100644 index 00000000..a34fff36 --- /dev/null +++ b/pg-index-health-core/src/main/java/io/github/mfvanek/pg/checks/extractors/DuplicatedForeignKeysExtractor.java @@ -0,0 +1,59 @@ +/* + * 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.extractors; + +import io.github.mfvanek.pg.common.maintenance.ResultSetExtractor; +import io.github.mfvanek.pg.model.constraint.DuplicatedForeignKeys; +import io.github.mfvanek.pg.model.constraint.ForeignKey; + +import java.sql.ResultSet; +import java.sql.SQLException; +import javax.annotation.Nonnull; + +/** + * A mapper from raw data to {@link DuplicatedForeignKeys} model. + * + * @author Ivan Vahrushev + * @see ForeignKeyExtractor + * @since 0.13.1 + */ +public class DuplicatedForeignKeysExtractor implements ResultSetExtractor { + + private final ResultSetExtractor defaultExtractor; + private final ResultSetExtractor duplicateKeyExtractor; + + private DuplicatedForeignKeysExtractor(@Nonnull final String prefix) { + this.defaultExtractor = ForeignKeyExtractor.ofDefault(); + this.duplicateKeyExtractor = ForeignKeyExtractor.withPrefix(prefix); + } + + /** + * {@inheritDoc} + */ + @Nonnull + @Override + public DuplicatedForeignKeys extractData(@Nonnull final ResultSet resultSet) throws SQLException { + final ForeignKey first = defaultExtractor.extractData(resultSet); + final ForeignKey second = duplicateKeyExtractor.extractData(resultSet); + return DuplicatedForeignKeys.of(first, second); + } + + /** + * Creates {@code DuplicatedForeignKeysExtractor} instance. + * + * @param prefix given prefix; must be non-null + * @return {@code DuplicatedForeignKeysExtractor} instance + */ + @Nonnull + public static ResultSetExtractor of(@Nonnull final String prefix) { + return new DuplicatedForeignKeysExtractor(prefix); + } +} diff --git a/pg-index-health-core/src/main/java/io/github/mfvanek/pg/checks/extractors/ForeignKeyExtractor.java b/pg-index-health-core/src/main/java/io/github/mfvanek/pg/checks/extractors/ForeignKeyExtractor.java new file mode 100644 index 00000000..5126c03d --- /dev/null +++ b/pg-index-health-core/src/main/java/io/github/mfvanek/pg/checks/extractors/ForeignKeyExtractor.java @@ -0,0 +1,93 @@ +/* + * 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.extractors; + +import io.github.mfvanek.pg.common.maintenance.ResultSetExtractor; +import io.github.mfvanek.pg.model.column.Column; +import io.github.mfvanek.pg.model.constraint.ForeignKey; +import io.github.mfvanek.pg.utils.ColumnsInForeignKeyParser; + +import java.sql.Array; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; +import java.util.Objects; +import javax.annotation.Nonnull; + +import static io.github.mfvanek.pg.checks.extractors.TableExtractor.TABLE_NAME; + +/** + * A mapper from raw data to {@link ForeignKey} model. + * + * @author Ivan Vahrushev + * @since 0.13.1 + */ +public class ForeignKeyExtractor implements ResultSetExtractor { + + public static final String CONSTRAINT_NAME = "constraint_name"; + + private final String prefix; + + private ForeignKeyExtractor(@Nonnull final String prefix) { + this.prefix = Objects.requireNonNull(prefix, "prefix cannot be null"); + } + + /** + * {@inheritDoc} + */ + @Nonnull + @Override + public ForeignKey extractData(@Nonnull final ResultSet resultSet) throws SQLException { + final String tableName = resultSet.getString(TABLE_NAME); + final String constraintName = resultSet.getString(getConstraintNameField()); + final Array columnsArray = resultSet.getArray(getColumnsField()); + final String[] rawColumns = (String[]) columnsArray.getArray(); + final List columns = ColumnsInForeignKeyParser.parseRawColumnData(tableName, rawColumns); + return ForeignKey.of(tableName, constraintName, columns); + } + + @Nonnull + private String getConstraintNameField() { + if (!prefix.isBlank()) { + return prefix + "_" + CONSTRAINT_NAME; + } + return CONSTRAINT_NAME; + } + + @Nonnull + private String getColumnsField() { + if (!prefix.isBlank()) { + return prefix + "_constraint_columns"; + } + return "columns"; + } + + /** + * Creates default {@code ForeignKeyExtractor} instance. + * + * @return {@code ForeignKeyExtractor} instance + */ + @Nonnull + public static ResultSetExtractor ofDefault() { + return new ForeignKeyExtractor(""); + } + + /** + * Creates {@code ForeignKeyExtractor} instance for duplicated/intersected constraint fields with given prefix. + * + * @param prefix given prefix; must be non-null + * @return {@code ForeignKeyExtractor} instance + */ + @Nonnull + public static ResultSetExtractor withPrefix(@Nonnull final String prefix) { + return new ForeignKeyExtractor(prefix); + } +} diff --git a/pg-index-health-core/src/main/java/io/github/mfvanek/pg/checks/extractors/IndexWithSingleColumnExtractor.java b/pg-index-health-core/src/main/java/io/github/mfvanek/pg/checks/extractors/IndexWithSingleColumnExtractor.java index 8b27f15d..f04caf98 100644 --- a/pg-index-health-core/src/main/java/io/github/mfvanek/pg/checks/extractors/IndexWithSingleColumnExtractor.java +++ b/pg-index-health-core/src/main/java/io/github/mfvanek/pg/checks/extractors/IndexWithSingleColumnExtractor.java @@ -50,6 +50,11 @@ public IndexWithColumns extractData(@Nonnull final ResultSet resultSet) throws S return IndexWithColumns.ofSingle(tableName, indexName, indexSize, column); } + /** + * Creates {@code IndexWithSingleColumnExtractor} instance. + * + * @return {@code IndexWithSingleColumnExtractor} instance + */ @Nonnull public static ResultSetExtractor of() { return new IndexWithSingleColumnExtractor(); diff --git a/pg-index-health-core/src/main/java/io/github/mfvanek/pg/checks/extractors/TableExtractor.java b/pg-index-health-core/src/main/java/io/github/mfvanek/pg/checks/extractors/TableExtractor.java index e4eb0ebf..5c11d34b 100644 --- a/pg-index-health-core/src/main/java/io/github/mfvanek/pg/checks/extractors/TableExtractor.java +++ b/pg-index-health-core/src/main/java/io/github/mfvanek/pg/checks/extractors/TableExtractor.java @@ -42,6 +42,11 @@ public Table extractData(@Nonnull final ResultSet resultSet) throws SQLException return Table.of(tableName, tableSize); } + /** + * Creates {@code TableExtractor} instance. + * + * @return {@code TableExtractor} instance + */ @Nonnull public static ResultSetExtractor of() { return new TableExtractor(); diff --git a/pg-index-health-core/src/main/java/io/github/mfvanek/pg/checks/host/AbstractCheckOnHost.java b/pg-index-health-core/src/main/java/io/github/mfvanek/pg/checks/host/AbstractCheckOnHost.java index 7c0b2bc4..23368278 100644 --- a/pg-index-health-core/src/main/java/io/github/mfvanek/pg/checks/host/AbstractCheckOnHost.java +++ b/pg-index-health-core/src/main/java/io/github/mfvanek/pg/checks/host/AbstractCheckOnHost.java @@ -40,7 +40,6 @@ abstract class AbstractCheckOnHost implements DatabaseCheckO protected static final String INDEX_SIZE = IndexWithSingleColumnExtractor.INDEX_SIZE; protected static final String BLOAT_SIZE = "bloat_size"; protected static final String BLOAT_PERCENTAGE = "bloat_percentage"; - protected static final String CONSTRAINT_NAME = "constraint_name"; /** * An original java type representing database object. diff --git a/pg-index-health-core/src/main/java/io/github/mfvanek/pg/checks/host/DuplicatedForeignKeysCheckOnHost.java b/pg-index-health-core/src/main/java/io/github/mfvanek/pg/checks/host/DuplicatedForeignKeysCheckOnHost.java new file mode 100644 index 00000000..e7bb8652 --- /dev/null +++ b/pg-index-health-core/src/main/java/io/github/mfvanek/pg/checks/host/DuplicatedForeignKeysCheckOnHost.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.host; + +import io.github.mfvanek.pg.checks.extractors.DuplicatedForeignKeysExtractor; +import io.github.mfvanek.pg.common.maintenance.Diagnostic; +import io.github.mfvanek.pg.connection.PgConnection; +import io.github.mfvanek.pg.model.PgContext; +import io.github.mfvanek.pg.model.constraint.DuplicatedForeignKeys; + +import java.util.List; +import javax.annotation.Nonnull; + +/** + * Check for duplicated (completely identical) foreign keys on a specific host. + * + * @author Ivan Vahrushev + * @since 0.13.1 + */ +public class DuplicatedForeignKeysCheckOnHost extends AbstractCheckOnHost { + + /** + * Creates a new {@code DuplicatedForeignKeysCheckOnHost} object. + * + * @param pgConnection connection to the PostgreSQL database, must not be null + */ + public DuplicatedForeignKeysCheckOnHost(@Nonnull final PgConnection pgConnection) { + super(DuplicatedForeignKeys.class, pgConnection, Diagnostic.DUPLICATED_FOREIGN_KEYS); + } + + /** + * Returns duplicated (completely identical) foreign keys in the specified schema. + * + * @param pgContext check's context with the specified schema + * @return list of duplicated foreign keys + */ + @Nonnull + @Override + public List check(@Nonnull final PgContext pgContext) { + return executeQuery(pgContext, DuplicatedForeignKeysExtractor.of("duplicate")); + } +} diff --git a/pg-index-health-core/src/main/java/io/github/mfvanek/pg/checks/host/ForeignKeysNotCoveredWithIndexCheckOnHost.java b/pg-index-health-core/src/main/java/io/github/mfvanek/pg/checks/host/ForeignKeysNotCoveredWithIndexCheckOnHost.java index 0422a26f..64ad51b5 100644 --- a/pg-index-health-core/src/main/java/io/github/mfvanek/pg/checks/host/ForeignKeysNotCoveredWithIndexCheckOnHost.java +++ b/pg-index-health-core/src/main/java/io/github/mfvanek/pg/checks/host/ForeignKeysNotCoveredWithIndexCheckOnHost.java @@ -10,14 +10,12 @@ package io.github.mfvanek.pg.checks.host; +import io.github.mfvanek.pg.checks.extractors.ForeignKeyExtractor; import io.github.mfvanek.pg.common.maintenance.Diagnostic; 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.constraint.ForeignKey; -import io.github.mfvanek.pg.utils.ColumnsInForeignKeyParser; -import java.sql.Array; import java.util.List; import javax.annotation.Nonnull; @@ -42,13 +40,6 @@ public ForeignKeysNotCoveredWithIndexCheckOnHost(@Nonnull final PgConnection pgC @Nonnull @Override public List check(@Nonnull final PgContext pgContext) { - return executeQuery(pgContext, rs -> { - final String tableName = rs.getString(TABLE_NAME); - final String constraintName = rs.getString(CONSTRAINT_NAME); - final Array columnsArray = rs.getArray("columns"); - final String[] rawColumns = (String[]) columnsArray.getArray(); - final List columns = ColumnsInForeignKeyParser.parseRawColumnData(tableName, rawColumns); - return ForeignKey.of(tableName, constraintName, columns); - }); + return executeQuery(pgContext, ForeignKeyExtractor.ofDefault()); } } diff --git a/pg-index-health-core/src/main/java/io/github/mfvanek/pg/checks/host/IntersectedForeignKeysCheckOnHost.java b/pg-index-health-core/src/main/java/io/github/mfvanek/pg/checks/host/IntersectedForeignKeysCheckOnHost.java new file mode 100644 index 00000000..ff478910 --- /dev/null +++ b/pg-index-health-core/src/main/java/io/github/mfvanek/pg/checks/host/IntersectedForeignKeysCheckOnHost.java @@ -0,0 +1,52 @@ +/* + * 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.DuplicatedForeignKeysExtractor; +import io.github.mfvanek.pg.common.maintenance.Diagnostic; +import io.github.mfvanek.pg.connection.PgConnection; +import io.github.mfvanek.pg.model.PgContext; +import io.github.mfvanek.pg.model.constraint.DuplicatedForeignKeys; + +import java.util.List; +import javax.annotation.Nonnull; + +/** + * Check for intersected (partially identical) foreign keys on a specific host. + * + * @author Ivan Vahrushev + * @see DuplicatedForeignKeysCheckOnHost + * @since 0.13.1 + */ +public class IntersectedForeignKeysCheckOnHost extends AbstractCheckOnHost { + + /** + * Creates a new {@code IntersectedForeignKeysCheckOnHost} object. + * + * @param pgConnection connection to the PostgreSQL database, must not be null + */ + public IntersectedForeignKeysCheckOnHost(@Nonnull final PgConnection pgConnection) { + super(DuplicatedForeignKeys.class, pgConnection, Diagnostic.INTERSECTED_FOREIGN_KEYS); + } + + /** + * Returns intersected (partially identical) foreign keys in the specified schema (except completely identical). + * + * @param pgContext check's context with the specified schema + * @return list of intersected foreign keys + * @see DuplicatedForeignKeysCheckOnHost + */ + @Nonnull + @Override + public List check(@Nonnull final PgContext pgContext) { + return executeQuery(pgContext, DuplicatedForeignKeysExtractor.of("intersected")); + } +} diff --git a/pg-index-health-core/src/main/java/io/github/mfvanek/pg/checks/host/NotValidConstraintsCheckOnHost.java b/pg-index-health-core/src/main/java/io/github/mfvanek/pg/checks/host/NotValidConstraintsCheckOnHost.java index ec92bd7b..3879d450 100644 --- a/pg-index-health-core/src/main/java/io/github/mfvanek/pg/checks/host/NotValidConstraintsCheckOnHost.java +++ b/pg-index-health-core/src/main/java/io/github/mfvanek/pg/checks/host/NotValidConstraintsCheckOnHost.java @@ -19,6 +19,8 @@ import java.util.List; import javax.annotation.Nonnull; +import static io.github.mfvanek.pg.checks.extractors.ForeignKeyExtractor.CONSTRAINT_NAME; + /** * Check for not valid constraints on a specific host. * diff --git a/pg-index-health-core/src/main/java/io/github/mfvanek/pg/common/maintenance/Diagnostic.java b/pg-index-health-core/src/main/java/io/github/mfvanek/pg/common/maintenance/Diagnostic.java index 8cf461b2..8d3bb192 100644 --- a/pg-index-health-core/src/main/java/io/github/mfvanek/pg/common/maintenance/Diagnostic.java +++ b/pg-index-health-core/src/main/java/io/github/mfvanek/pg/common/maintenance/Diagnostic.java @@ -43,7 +43,9 @@ public enum Diagnostic { NOT_VALID_CONSTRAINTS(ExecutionTopology.ON_PRIMARY, "check_not_valid_constraints.sql", QueryExecutors::executeQueryWithSchema), BTREE_INDEXES_ON_ARRAY_COLUMNS(ExecutionTopology.ON_PRIMARY, "btree_indexes_on_array_columns.sql", QueryExecutors::executeQueryWithSchema), SEQUENCE_OVERFLOW(ExecutionTopology.ON_PRIMARY, "sequence_overflow.sql", QueryExecutors::executeQueryWithRemainingPercentageThreshold), - PRIMARY_KEYS_WITH_SERIAL_TYPES(ExecutionTopology.ON_PRIMARY, "primary_keys_with_serial_types.sql", QueryExecutors::executeQueryWithSchema); + PRIMARY_KEYS_WITH_SERIAL_TYPES(ExecutionTopology.ON_PRIMARY, "primary_keys_with_serial_types.sql", QueryExecutors::executeQueryWithSchema), + DUPLICATED_FOREIGN_KEYS(ExecutionTopology.ON_PRIMARY, "duplicated_foreign_keys.sql", QueryExecutors::executeQueryWithSchema), + INTERSECTED_FOREIGN_KEYS(ExecutionTopology.ON_PRIMARY, "intersected_foreign_keys.sql", QueryExecutors::executeQueryWithSchema); private final ExecutionTopology executionTopology; private final String sqlQueryFileName; diff --git a/pg-index-health-core/src/test/java/io/github/mfvanek/pg/checks/host/DuplicatedForeignKeysCheckOnHostTest.java b/pg-index-health-core/src/test/java/io/github/mfvanek/pg/checks/host/DuplicatedForeignKeysCheckOnHostTest.java new file mode 100644 index 00000000..12e326e3 --- /dev/null +++ b/pg-index-health-core/src/test/java/io/github/mfvanek/pg/checks/host/DuplicatedForeignKeysCheckOnHostTest.java @@ -0,0 +1,55 @@ +/* + * 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.constraint.DuplicatedForeignKeys; +import io.github.mfvanek.pg.model.constraint.ForeignKey; +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 DuplicatedForeignKeysCheckOnHostTest extends DatabaseAwareTestBase { + + private final DatabaseCheckOnHost check = new DuplicatedForeignKeysCheckOnHost(getPgConnection()); + + @Test + void shouldSatisfyContract() { + assertThat(check) + .hasType(DuplicatedForeignKeys.class) + .hasDiagnostic(Diagnostic.DUPLICATED_FOREIGN_KEYS) + .hasHost(getHost()); + } + + @ParameterizedTest + @ValueSource(strings = {PgContext.DEFAULT_SCHEMA_NAME, "custom"}) + void onDatabaseWithThem(final String schemaName) { + executeTestOnDatabase(schemaName, dbp -> dbp.withReferences().withForeignKeyOnNullableColumn().withDuplicatedForeignKeys(), ctx -> { + final String expectedTableName = ctx.enrichWithSchema("accounts"); + assertThat(check) + .executing(ctx) + .hasSize(1) + .containsExactly( + DuplicatedForeignKeys.of( + ForeignKey.ofColumn(expectedTableName, "c_accounts_fk_client_id", + Column.ofNotNull(expectedTableName, "client_id")), + ForeignKey.ofColumn(expectedTableName, "c_accounts_fk_client_id_duplicate", + Column.ofNotNull(expectedTableName, "client_id"))) + ); + }); + } +} 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 89962ed2..49807ef4 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 @@ -42,13 +42,16 @@ void onDatabaseWithThem(final String schemaName) { Assertions.assertThat(existsStatisticsForTable(schemaName, "accounts")) .isTrue(); + final String accountsTableName = ctx.enrichWithSchema("accounts"); + final String clientsTableName = ctx.enrichWithSchema("clients"); assertThat(check) .executing(ctx) - .hasSize(3) + .hasSize(4) .containsExactlyInAnyOrder( - IndexWithBloat.of(ctx.enrichWithSchema("accounts"), ctx.enrichWithSchema("accounts_account_number_key"), 0L, 0L, 0), - IndexWithBloat.of(ctx.enrichWithSchema("accounts"), ctx.enrichWithSchema("accounts_pkey"), 0L, 0L, 0), - IndexWithBloat.of(ctx.enrichWithSchema("clients"), ctx.enrichWithSchema("clients_pkey"), 0L, 0L, 0)) + IndexWithBloat.of(accountsTableName, ctx.enrichWithSchema("accounts_account_number_key"), 0L, 0L, 0), + IndexWithBloat.of(accountsTableName, ctx.enrichWithSchema("accounts_pkey"), 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); }); diff --git a/pg-index-health-core/src/test/java/io/github/mfvanek/pg/checks/host/IntersectedForeignKeysCheckOnHostTest.java b/pg-index-health-core/src/test/java/io/github/mfvanek/pg/checks/host/IntersectedForeignKeysCheckOnHostTest.java new file mode 100644 index 00000000..8c19145c --- /dev/null +++ b/pg-index-health-core/src/test/java/io/github/mfvanek/pg/checks/host/IntersectedForeignKeysCheckOnHostTest.java @@ -0,0 +1,66 @@ +/* + * 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.constraint.DuplicatedForeignKeys; +import io.github.mfvanek.pg.model.constraint.ForeignKey; +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 java.util.List; + +import static io.github.mfvanek.pg.support.AbstractCheckOnHostAssert.assertThat; + +class IntersectedForeignKeysCheckOnHostTest extends DatabaseAwareTestBase { + + private final DatabaseCheckOnHost check = new IntersectedForeignKeysCheckOnHost(getPgConnection()); + + @Test + void shouldSatisfyContract() { + assertThat(check) + .hasType(DuplicatedForeignKeys.class) + .hasDiagnostic(Diagnostic.INTERSECTED_FOREIGN_KEYS) + .hasHost(getHost()); + } + + @ParameterizedTest + @ValueSource(strings = {PgContext.DEFAULT_SCHEMA_NAME, "custom"}) + void shouldIgnoreCompletelyIdenticalForeignKeys(final String schemaName) { + executeTestOnDatabase(schemaName, dbp -> dbp.withReferences().withForeignKeyOnNullableColumn().withDuplicatedForeignKeys(), ctx -> + assertThat(check) + .executing(ctx) + .isEmpty()); + } + + @ParameterizedTest + @ValueSource(strings = {PgContext.DEFAULT_SCHEMA_NAME, "custom"}) + void onDatabaseWithThem(final String schemaName) { + executeTestOnDatabase(schemaName, dbp -> dbp.withReferences().withForeignKeyOnNullableColumn().withIntersectedForeignKeys(), ctx -> { + final String expectedTableName = ctx.enrichWithSchema("client_preferences"); + assertThat(check) + .executing(ctx) + .hasSize(1) + .containsExactly( + DuplicatedForeignKeys.of( + ForeignKey.of(expectedTableName, "c_client_preferences_email_phone_fk", + List.of(Column.ofNotNull(expectedTableName, "email"), Column.ofNotNull(expectedTableName, "phone"))), + ForeignKey.of(expectedTableName, "c_client_preferences_phone_email_fk", + List.of(Column.ofNotNull(expectedTableName, "phone"), Column.ofNotNull(expectedTableName, "email")))) + ); + }); + } +} diff --git a/pg-index-health-core/src/testFixtures/java/io/github/mfvanek/pg/support/DatabasePopulator.java b/pg-index-health-core/src/testFixtures/java/io/github/mfvanek/pg/support/DatabasePopulator.java index 61bcbbd2..cf62fc3d 100644 --- a/pg-index-health-core/src/testFixtures/java/io/github/mfvanek/pg/support/DatabasePopulator.java +++ b/pg-index-health-core/src/testFixtures/java/io/github/mfvanek/pg/support/DatabasePopulator.java @@ -18,6 +18,8 @@ import io.github.mfvanek.pg.support.statements.AddCommentOnFunctionsStatement; import io.github.mfvanek.pg.support.statements.AddCommentOnProceduresStatement; import io.github.mfvanek.pg.support.statements.AddCommentOnTablesStatement; +import io.github.mfvanek.pg.support.statements.AddDuplicatedForeignKeysStatement; +import io.github.mfvanek.pg.support.statements.AddIntersectedForeignKeysStatement; import io.github.mfvanek.pg.support.statements.AddInvalidForeignKeyStatement; import io.github.mfvanek.pg.support.statements.AddLinksBetweenAccountsAndClientsStatement; import io.github.mfvanek.pg.support.statements.ConvertColumnToJsonTypeStatement; @@ -295,6 +297,18 @@ public DatabasePopulator withIdentityPrimaryKey() { return this; } + @Nonnull + public DatabasePopulator withDuplicatedForeignKeys() { + statementsToExecuteInSameTransaction.putIfAbsent(105, new AddDuplicatedForeignKeysStatement()); + return this; + } + + @Nonnull + public DatabasePopulator withIntersectedForeignKeys() { + statementsToExecuteInSameTransaction.putIfAbsent(106, new AddIntersectedForeignKeysStatement()); + return this; + } + public void populate() { try (SchemaNameHolder ignored = SchemaNameHolder.with(schemaName)) { ExecuteUtils.executeInTransaction(dataSource, statementsToExecuteInSameTransaction.values()); diff --git a/pg-index-health-core/src/testFixtures/java/io/github/mfvanek/pg/support/statements/AddDuplicatedForeignKeysStatement.java b/pg-index-health-core/src/testFixtures/java/io/github/mfvanek/pg/support/statements/AddDuplicatedForeignKeysStatement.java new file mode 100644 index 00000000..1970cc62 --- /dev/null +++ b/pg-index-health-core/src/testFixtures/java/io/github/mfvanek/pg/support/statements/AddDuplicatedForeignKeysStatement.java @@ -0,0 +1,25 @@ +/* + * 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.util.List; +import javax.annotation.Nonnull; + +public class AddDuplicatedForeignKeysStatement extends AbstractDbStatement { + + @Nonnull + @Override + protected List getSqlToExecute() { + return List.of("alter table if exists {schemaName}.accounts " + + "add constraint c_accounts_fk_client_id_duplicate foreign key (client_id) references {schemaName}.clients (id);" + ); + } +} diff --git a/pg-index-health-core/src/testFixtures/java/io/github/mfvanek/pg/support/statements/AddIntersectedForeignKeysStatement.java b/pg-index-health-core/src/testFixtures/java/io/github/mfvanek/pg/support/statements/AddIntersectedForeignKeysStatement.java new file mode 100644 index 00000000..1a98c5ca --- /dev/null +++ b/pg-index-health-core/src/testFixtures/java/io/github/mfvanek/pg/support/statements/AddIntersectedForeignKeysStatement.java @@ -0,0 +1,34 @@ +/* + * 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.util.List; +import javax.annotation.Nonnull; + +public class AddIntersectedForeignKeysStatement extends AbstractDbStatement { + + @Nonnull + @Override + protected List getSqlToExecute() { + return List.of( + "create table if not exists {schemaName}.client_preferences (" + + "id bigint not null generated always as identity," + + "email varchar(200) not null," + + "phone varchar(50) not null," + + "call_time_start timetz not null," + + "call_time_end timetz not null)", + "alter table if exists {schemaName}.client_preferences " + + "add constraint c_client_preferences_email_phone_fk foreign key (email, phone) references {schemaName}.clients (email, phone)", + "alter table if exists {schemaName}.client_preferences " + + "add constraint c_client_preferences_phone_email_fk foreign key (phone, email) references {schemaName}.clients (phone, email)" + ); + } +} diff --git a/pg-index-health-core/src/testFixtures/java/io/github/mfvanek/pg/support/statements/CreateClientsTableStatement.java b/pg-index-health-core/src/testFixtures/java/io/github/mfvanek/pg/support/statements/CreateClientsTableStatement.java index 3cebb3ef..5d8d6d1f 100644 --- a/pg-index-health-core/src/testFixtures/java/io/github/mfvanek/pg/support/statements/CreateClientsTableStatement.java +++ b/pg-index-health-core/src/testFixtures/java/io/github/mfvanek/pg/support/statements/CreateClientsTableStatement.java @@ -25,7 +25,12 @@ protected List getSqlToExecute() { "last_name varchar(255) not null," + "first_name varchar(255) not null," + "middle_name varchar(255)," + - "info jsonb)" + "info jsonb," + + "email varchar(200) not null," + + "phone varchar(50) not null)", + "create unique index if not exists i_clients_email_phone on {schemaName}.clients (email, phone)", + "comment on column {schemaName}.clients.email is 'Customer''s email';", + "comment on column {schemaName}.clients.phone is 'Customer''s phone number';" ); } } diff --git a/pg-index-health-core/src/testFixtures/java/io/github/mfvanek/pg/support/statements/CreateTableWithIdentityPrimaryKey.java b/pg-index-health-core/src/testFixtures/java/io/github/mfvanek/pg/support/statements/CreateTableWithIdentityPrimaryKey.java index 8f63b43e..37c5878b 100644 --- a/pg-index-health-core/src/testFixtures/java/io/github/mfvanek/pg/support/statements/CreateTableWithIdentityPrimaryKey.java +++ b/pg-index-health-core/src/testFixtures/java/io/github/mfvanek/pg/support/statements/CreateTableWithIdentityPrimaryKey.java @@ -19,7 +19,7 @@ public class CreateTableWithIdentityPrimaryKey extends AbstractDbStatement { @Override protected List getSqlToExecute() { return List.of("create table if not exists {schemaName}.test_table_with_identity_pk(" + - "id bigint not null generated always as identity, " + + "id bigint not null generated always as identity," + "num bigserial);" ); } diff --git a/pg-index-health-core/src/testFixtures/java/io/github/mfvanek/pg/support/statements/InsertDataIntoTablesAction.java b/pg-index-health-core/src/testFixtures/java/io/github/mfvanek/pg/support/statements/InsertDataIntoTablesAction.java index 56398f40..b22eaf09 100644 --- a/pg-index-health-core/src/testFixtures/java/io/github/mfvanek/pg/support/statements/InsertDataIntoTablesAction.java +++ b/pg-index-health-core/src/testFixtures/java/io/github/mfvanek/pg/support/statements/InsertDataIntoTablesAction.java @@ -39,7 +39,7 @@ public InsertDataIntoTablesAction(@Nonnull final DataSource dataSource, @Nonnull public void run() { final int clientsCountToCreate = 1_000; final String insertClientSql = String.format( - Locale.ROOT, "insert into %s.clients (id, first_name, last_name, info) values (?, ?, ?, ?)", schemaName); + Locale.ROOT, "insert into %s.clients (id, first_name, last_name, info, email, phone) values (?, ?, ?, ?, ?, ?)", schemaName); final String insertAccountSql = String.format( Locale.ROOT, "insert into %s.accounts (client_id, account_number) values (?, ?)", schemaName); try (Connection connection = dataSource.getConnection(); @@ -48,12 +48,17 @@ public void run() { connection.setAutoCommit(false); for (int counter = 0; counter < clientsCountToCreate; ++counter) { final long clientId = getNextClientIdFromSequence(connection); - final String lastName = RandomStringUtils.randomAlphabetic(10); - final String firstName = RandomStringUtils.randomAlphabetic(10); + final String lastName = RandomStringUtils.secureStrong().nextAlphabetic(15); + final String firstName = RandomStringUtils.secureStrong().nextAlphabetic(10); + final String domainName = RandomStringUtils.secureStrong().nextAlphabetic(8); + final String email = lastName + "_" + firstName + "@" + domainName + ".com"; + final String phone = RandomStringUtils.secureStrong().nextAlphanumeric(10); insertClientStatement.setLong(1, clientId); insertClientStatement.setString(2, firstName); insertClientStatement.setString(3, lastName); insertClientStatement.setObject(4, prepareClientInfo()); + insertClientStatement.setString(5, email); + insertClientStatement.setString(6, phone); insertClientStatement.executeUpdate(); final String accountNumber = generateAccountNumber(clientId); @@ -64,6 +69,7 @@ public void run() { // Insert at least one duplicated client row final long clientId = getNextClientIdFromSequence(connection); insertClientStatement.setLong(1, clientId); + insertClientStatement.setString(6, clientId + "unique_phone"); insertClientStatement.executeUpdate(); connection.commit(); } catch (SQLException e) { diff --git a/pg-index-health-logger/src/main/java/io/github/mfvanek/pg/common/health/logger/AbstractHealthLogger.java b/pg-index-health-logger/src/main/java/io/github/mfvanek/pg/common/health/logger/AbstractHealthLogger.java index ebe61f9b..16381545 100644 --- a/pg-index-health-logger/src/main/java/io/github/mfvanek/pg/common/health/logger/AbstractHealthLogger.java +++ b/pg-index-health-logger/src/main/java/io/github/mfvanek/pg/common/health/logger/AbstractHealthLogger.java @@ -28,6 +28,7 @@ import io.github.mfvanek.pg.model.column.Column; import io.github.mfvanek.pg.model.column.ColumnWithSerialType; import io.github.mfvanek.pg.model.constraint.Constraint; +import io.github.mfvanek.pg.model.constraint.DuplicatedForeignKeys; import io.github.mfvanek.pg.model.constraint.ForeignKey; import io.github.mfvanek.pg.model.function.StoredFunction; import io.github.mfvanek.pg.model.index.DuplicatedIndexes; @@ -102,6 +103,8 @@ public final List logAll(@Nonnull final Exclusions exclusions, logResult.add(logBtreeIndexesOnArrayColumns(databaseChecks, exclusions, pgContext)); logResult.add(logSequenceOverflow(databaseChecks, pgContext)); logResult.add(logPrimaryKeysWithSerialTypes(databaseChecks, pgContext)); + logResult.add(logDuplicatedForeignKeys(databaseChecks, pgContext)); + logResult.add(logIntersectedForeignKeys(databaseChecks, pgContext)); return logResult; } @@ -253,7 +256,7 @@ private String logNotValidConstraints(@Nonnull final DatabaseChecks databaseChec @Nonnull private String logSequenceOverflow(@Nonnull final DatabaseChecks databaseChecks, - @Nonnull final PgContext pgContext) { + @Nonnull final PgContext pgContext) { return logCheckResult(databaseChecks.getCheck(Diagnostic.SEQUENCE_OVERFLOW, SequenceState.class), c -> true, pgContext, SimpleLoggingKey.SEQUENCE_OVERFLOW); } @@ -265,6 +268,20 @@ private String logPrimaryKeysWithSerialTypes(@Nonnull final DatabaseChecks datab c -> true, pgContext, SimpleLoggingKey.PRIMARY_KEYS_WITH_SERIAL_TYPES); } + @Nonnull + private String logDuplicatedForeignKeys(@Nonnull final DatabaseChecks databaseChecks, + @Nonnull final PgContext pgContext) { + return logCheckResult(databaseChecks.getCheck(Diagnostic.DUPLICATED_FOREIGN_KEYS, DuplicatedForeignKeys.class), + c -> true, pgContext, SimpleLoggingKey.DUPLICATED_FOREIGN_KEYS); + } + + @Nonnull + private String logIntersectedForeignKeys(@Nonnull final DatabaseChecks databaseChecks, + @Nonnull final PgContext pgContext) { + return logCheckResult(databaseChecks.getCheck(Diagnostic.INTERSECTED_FOREIGN_KEYS, DuplicatedForeignKeys.class), + c -> true, pgContext, SimpleLoggingKey.INTERSECTED_FOREIGN_KEYS); + } + @Nonnull private String logCheckResult(@Nonnull final DatabaseCheckOnCluster check, @Nonnull final Predicate exclusionsFilter, diff --git a/pg-index-health-logger/src/main/java/io/github/mfvanek/pg/common/health/logger/SimpleLoggingKey.java b/pg-index-health-logger/src/main/java/io/github/mfvanek/pg/common/health/logger/SimpleLoggingKey.java index ded49865..d1da6ec1 100644 --- a/pg-index-health-logger/src/main/java/io/github/mfvanek/pg/common/health/logger/SimpleLoggingKey.java +++ b/pg-index-health-logger/src/main/java/io/github/mfvanek/pg/common/health/logger/SimpleLoggingKey.java @@ -34,7 +34,9 @@ public enum SimpleLoggingKey implements LoggingKey { NOT_VALID_CONSTRAINTS("not_valid_constraints"), BTREE_INDEXES_ON_ARRAY_COLUMNS("btree_indexes_on_array_columns"), SEQUENCE_OVERFLOW("sequence_overflow"), - PRIMARY_KEYS_WITH_SERIAL_TYPES("primary_keys_with_serial_types"); + PRIMARY_KEYS_WITH_SERIAL_TYPES("primary_keys_with_serial_types"), + DUPLICATED_FOREIGN_KEYS("duplicated_foreign_keys"), + INTERSECTED_FOREIGN_KEYS("intersected_foreign_keys"); private final String subKeyName; diff --git a/pg-index-health-logger/src/main/java/io/github/mfvanek/pg/common/maintenance/DatabaseChecks.java b/pg-index-health-logger/src/main/java/io/github/mfvanek/pg/common/maintenance/DatabaseChecks.java index 08f80ca6..241f5239 100644 --- a/pg-index-health-logger/src/main/java/io/github/mfvanek/pg/common/maintenance/DatabaseChecks.java +++ b/pg-index-health-logger/src/main/java/io/github/mfvanek/pg/common/maintenance/DatabaseChecks.java @@ -14,12 +14,14 @@ import io.github.mfvanek.pg.checks.cluster.ColumnsWithJsonTypeCheckOnCluster; import io.github.mfvanek.pg.checks.cluster.ColumnsWithSerialTypesCheckOnCluster; import io.github.mfvanek.pg.checks.cluster.ColumnsWithoutDescriptionCheckOnCluster; +import io.github.mfvanek.pg.checks.cluster.DuplicatedForeignKeysCheckOnCluster; import io.github.mfvanek.pg.checks.cluster.DuplicatedIndexesCheckOnCluster; 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.IntersectedForeignKeysCheckOnCluster; import io.github.mfvanek.pg.checks.cluster.IntersectedIndexesCheckOnCluster; import io.github.mfvanek.pg.checks.cluster.InvalidIndexesCheckOnCluster; import io.github.mfvanek.pg.checks.cluster.NotValidConstraintsCheckOnCluster; @@ -67,7 +69,9 @@ public DatabaseChecks(@Nonnull final HighAvailabilityPgConnection haPgConnection new NotValidConstraintsCheckOnCluster(haPgConnection), new BtreeIndexesOnArrayColumnsCheckOnCluster(haPgConnection), new SequenceOverflowCheckOnCluster(haPgConnection), - new PrimaryKeysWithSerialTypesCheckOnCluster(haPgConnection) + new PrimaryKeysWithSerialTypesCheckOnCluster(haPgConnection), + new DuplicatedForeignKeysCheckOnCluster(haPgConnection), + new IntersectedForeignKeysCheckOnCluster(haPgConnection) ); allChecks.forEach(check -> this.checks.putIfAbsent(check.getDiagnostic(), check)); } diff --git a/pg-index-health-logger/src/test/java/io/github/mfvanek/pg/common/health/logger/HealthLoggerTest.java b/pg-index-health-logger/src/test/java/io/github/mfvanek/pg/common/health/logger/HealthLoggerTest.java index c9da2826..6de116bb 100644 --- a/pg-index-health-logger/src/test/java/io/github/mfvanek/pg/common/health/logger/HealthLoggerTest.java +++ b/pg-index-health-logger/src/test/java/io/github/mfvanek/pg/common/health/logger/HealthLoggerTest.java @@ -71,7 +71,9 @@ void logAll(final String schemaName) { .withFunctions() .withNotValidConstraints() .withBtreeIndexesOnArrayColumn() - .withSequenceOverflow(), + .withSequenceOverflow() + .withDuplicatedForeignKeys() + .withIntersectedForeignKeys(), ctx -> { collectStatistics(schemaName); assertThat(logger.logAll(Exclusions.empty(), ctx)) @@ -79,16 +81,16 @@ void logAll(final String schemaName) { .containsExactlyInAnyOrder( "1999-12-31T23:59:59Z\tdb_indexes_health\tinvalid_indexes\t1", "1999-12-31T23:59:59Z\tdb_indexes_health\tduplicated_indexes\t2", - "1999-12-31T23:59:59Z\tdb_indexes_health\tforeign_keys_without_index\t2", - "1999-12-31T23:59:59Z\tdb_indexes_health\ttables_without_primary_key\t1", + "1999-12-31T23:59:59Z\tdb_indexes_health\tforeign_keys_without_index\t5", + "1999-12-31T23:59:59Z\tdb_indexes_health\ttables_without_primary_key\t2", "1999-12-31T23:59:59Z\tdb_indexes_health\tindexes_with_null_values\t1", - "1999-12-31T23:59:59Z\tdb_indexes_health\tindexes_with_bloat\t16", + "1999-12-31T23:59:59Z\tdb_indexes_health\tindexes_with_bloat\t17", "1999-12-31T23:59:59Z\tdb_indexes_health\ttables_with_bloat\t2", "1999-12-31T23:59:59Z\tdb_indexes_health\tintersected_indexes\t11", "1999-12-31T23:59:59Z\tdb_indexes_health\tunused_indexes\t12", "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\t18", + "1999-12-31T23:59:59Z\tdb_indexes_health\ttables_without_description\t5", + "1999-12-31T23:59:59Z\tdb_indexes_health\tcolumns_without_description\t23", "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", @@ -96,7 +98,9 @@ void logAll(final String schemaName) { "1999-12-31T23:59:59Z\tdb_indexes_health\tnot_valid_constraints\t2", "1999-12-31T23:59:59Z\tdb_indexes_health\tbtree_indexes_on_array_columns\t2", "1999-12-31T23:59:59Z\tdb_indexes_health\tsequence_overflow\t3", - "1999-12-31T23:59:59Z\tdb_indexes_health\tprimary_keys_with_serial_types\t1" + "1999-12-31T23:59:59Z\tdb_indexes_health\tprimary_keys_with_serial_types\t1", + "1999-12-31T23:59:59Z\tdb_indexes_health\tduplicated_foreign_keys\t3", + "1999-12-31T23:59:59Z\tdb_indexes_health\tintersected_foreign_keys\t1" ); } ); diff --git a/pg-index-health-logger/src/test/java/io/github/mfvanek/pg/common/health/logger/StandardHealthLoggerTest.java b/pg-index-health-logger/src/test/java/io/github/mfvanek/pg/common/health/logger/StandardHealthLoggerTest.java index b27eb778..b7d1351b 100644 --- a/pg-index-health-logger/src/test/java/io/github/mfvanek/pg/common/health/logger/StandardHealthLoggerTest.java +++ b/pg-index-health-logger/src/test/java/io/github/mfvanek/pg/common/health/logger/StandardHealthLoggerTest.java @@ -47,7 +47,9 @@ void logAll(final String schemaName) { .withJsonType() .withSerialType() .withFunctions() - .withNotValidConstraints(), + .withNotValidConstraints() + .withDuplicatedForeignKeys() + .withIntersectedForeignKeys(), ctx -> { collectStatistics(schemaName); final List logs = logger.logAll(Exclusions.empty(), ctx); @@ -56,16 +58,16 @@ void logAll(final String schemaName) { .containsExactlyInAnyOrder( "invalid_indexes:1", "duplicated_indexes:2", - "foreign_keys_without_index:2", - "tables_without_primary_key:1", + "foreign_keys_without_index:5", + "tables_without_primary_key:2", "indexes_with_null_values:1", - "indexes_with_bloat:16", + "indexes_with_bloat:17", "tables_with_bloat:2", "intersected_indexes:11", "unused_indexes:12", "tables_with_missing_indexes:0", - "tables_without_description:4", - "columns_without_description:18", + "tables_without_description:5", + "columns_without_description:23", "columns_with_json_type:1", "columns_with_serial_types:2", "functions_without_description:2", @@ -73,7 +75,9 @@ void logAll(final String schemaName) { "not_valid_constraints:2", "btree_indexes_on_array_columns:2", "sequence_overflow:0", - "primary_keys_with_serial_types:1"); + "primary_keys_with_serial_types:1", + "duplicated_foreign_keys:3", + "intersected_foreign_keys:1"); }); } diff --git a/pg-index-health-model/src/main/java/io/github/mfvanek/pg/model/constraint/DuplicatedForeignKeys.java b/pg-index-health-model/src/main/java/io/github/mfvanek/pg/model/constraint/DuplicatedForeignKeys.java index f49d0baa..33afae02 100644 --- a/pg-index-health-model/src/main/java/io/github/mfvanek/pg/model/constraint/DuplicatedForeignKeys.java +++ b/pg-index-health-model/src/main/java/io/github/mfvanek/pg/model/constraint/DuplicatedForeignKeys.java @@ -26,6 +26,7 @@ * * @author Ivan Vakhrushev * @see TableNameAware + * @since 0.13.1 */ @Immutable public class DuplicatedForeignKeys implements DbObject, TableNameAware { diff --git a/pg-index-health/src/main/java/io/github/mfvanek/pg/checks/cluster/DuplicatedForeignKeysCheckOnCluster.java b/pg-index-health/src/main/java/io/github/mfvanek/pg/checks/cluster/DuplicatedForeignKeysCheckOnCluster.java new file mode 100644 index 00000000..e0ee4396 --- /dev/null +++ b/pg-index-health/src/main/java/io/github/mfvanek/pg/checks/cluster/DuplicatedForeignKeysCheckOnCluster.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.DuplicatedForeignKeysCheckOnHost; +import io.github.mfvanek.pg.connection.HighAvailabilityPgConnection; +import io.github.mfvanek.pg.model.constraint.DuplicatedForeignKeys; + +import javax.annotation.Nonnull; + +/** + * Check for duplicated (completely identical) foreign keys on all hosts in the cluster. + * + * @author Ivan Vahrushev + * @since 0.13.1 + */ +public class DuplicatedForeignKeysCheckOnCluster extends AbstractCheckOnCluster { + + public DuplicatedForeignKeysCheckOnCluster(@Nonnull final HighAvailabilityPgConnection haPgConnection) { + super(haPgConnection, DuplicatedForeignKeysCheckOnHost::new); + } +} diff --git a/pg-index-health/src/main/java/io/github/mfvanek/pg/checks/cluster/IntersectedForeignKeysCheckOnCluster.java b/pg-index-health/src/main/java/io/github/mfvanek/pg/checks/cluster/IntersectedForeignKeysCheckOnCluster.java new file mode 100644 index 00000000..c56314b4 --- /dev/null +++ b/pg-index-health/src/main/java/io/github/mfvanek/pg/checks/cluster/IntersectedForeignKeysCheckOnCluster.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.IntersectedForeignKeysCheckOnHost; +import io.github.mfvanek.pg.connection.HighAvailabilityPgConnection; +import io.github.mfvanek.pg.model.constraint.DuplicatedForeignKeys; + +import javax.annotation.Nonnull; + +/** + * Check for intersected (partially identical) foreign keys on all hosts in the cluster. + * + * @author Ivan Vahrushev + * @since 0.13.1 + */ +public class IntersectedForeignKeysCheckOnCluster extends AbstractCheckOnCluster { + + public IntersectedForeignKeysCheckOnCluster(@Nonnull final HighAvailabilityPgConnection haPgConnection) { + super(haPgConnection, IntersectedForeignKeysCheckOnHost::new); + } +} diff --git a/pg-index-health/src/test/java/io/github/mfvanek/pg/checks/cluster/DuplicatedForeignKeysCheckOnClusterTest.java b/pg-index-health/src/test/java/io/github/mfvanek/pg/checks/cluster/DuplicatedForeignKeysCheckOnClusterTest.java new file mode 100644 index 00000000..6db07845 --- /dev/null +++ b/pg-index-health/src/test/java/io/github/mfvanek/pg/checks/cluster/DuplicatedForeignKeysCheckOnClusterTest.java @@ -0,0 +1,56 @@ +/* + * 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.FilterTablesByNamePredicate; +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.constraint.DuplicatedForeignKeys; +import io.github.mfvanek.pg.model.constraint.ForeignKey; +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 DuplicatedForeignKeysCheckOnClusterTest extends DatabaseAwareTestBase { + + private final DatabaseCheckOnCluster check = new DuplicatedForeignKeysCheckOnCluster(getHaPgConnection()); + + @Test + void shouldSatisfyContract() { + assertThat(check.getType()).isEqualTo(DuplicatedForeignKeys.class); + assertThat(check.getDiagnostic()).isEqualTo(Diagnostic.DUPLICATED_FOREIGN_KEYS); + } + + @ParameterizedTest + @ValueSource(strings = {PgContext.DEFAULT_SCHEMA_NAME, "custom"}) + void onDatabaseWithThem(final String schemaName) { + executeTestOnDatabase(schemaName, dbp -> dbp.withReferences().withForeignKeyOnNullableColumn().withDuplicatedForeignKeys(), ctx -> { + final String expectedTableName = ctx.enrichWithSchema("accounts"); + assertThat(check.check(ctx)) + .hasSize(1) + .containsExactly( + DuplicatedForeignKeys.of( + ForeignKey.ofColumn(expectedTableName, "c_accounts_fk_client_id", + Column.ofNotNull(expectedTableName, "client_id")), + ForeignKey.ofColumn(expectedTableName, "c_accounts_fk_client_id_duplicate", + Column.ofNotNull(expectedTableName, "client_id"))) + ); + + assertThat(check.check(ctx, FilterTablesByNamePredicate.of(expectedTableName))) + .isEmpty(); + }); + } +} 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 9ce9336d..9b9fbcc2 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 @@ -48,6 +48,7 @@ void onDatabaseWithoutThem(final String schemaName) { }); } + @SuppressWarnings("checkstyle:LambdaBodyLength") @ParameterizedTest @ValueSource(strings = {PgContext.DEFAULT_SCHEMA_NAME, "custom"}) void onDatabaseWithThem(final String schemaName) { @@ -56,22 +57,26 @@ void onDatabaseWithThem(final String schemaName) { assertThat(existsStatisticsForTable(schemaName, "accounts")) .isTrue(); + final String accountsTableName = ctx.enrichWithSchema("accounts"); + final String clientsTableName = ctx.enrichWithSchema("clients"); assertThat(check.check(ctx)) - .hasSize(3) + .hasSize(4) .containsExactlyInAnyOrder( - IndexWithBloat.of(ctx.enrichWithSchema("accounts"), ctx.enrichWithSchema("accounts_account_number_key"), 0L, 0L, 0), - IndexWithBloat.of(ctx.enrichWithSchema("accounts"), ctx.enrichWithSchema("accounts_pkey"), 0L, 0L, 0), - IndexWithBloat.of(ctx.enrichWithSchema("clients"), ctx.enrichWithSchema("clients_pkey"), 0L, 0L, 0)) + IndexWithBloat.of(accountsTableName, ctx.enrichWithSchema("accounts_account_number_key"), 0L, 0L, 0), + IndexWithBloat.of(accountsTableName, ctx.enrichWithSchema("accounts_pkey"), 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); final Predicate predicate = FilterIndexesBySizePredicate.of(1L) .and(FilterIndexesByNamePredicate.of(ctx.enrichWithSchema("accounts_pkey"))); assertThat(check.check(ctx, predicate)) - .hasSize(2) + .hasSize(3) .containsExactlyInAnyOrder( - IndexWithBloat.of(ctx.enrichWithSchema("accounts"), ctx.enrichWithSchema("accounts_account_number_key"), 0L, 0L, 0), - IndexWithBloat.of(ctx.enrichWithSchema("clients"), ctx.enrichWithSchema("clients_pkey"), 0L, 0L, 0)) + 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); diff --git a/pg-index-health/src/test/java/io/github/mfvanek/pg/checks/cluster/IntersectedForeignKeysCheckOnClusterTest.java b/pg-index-health/src/test/java/io/github/mfvanek/pg/checks/cluster/IntersectedForeignKeysCheckOnClusterTest.java new file mode 100644 index 00000000..a13092a4 --- /dev/null +++ b/pg-index-health/src/test/java/io/github/mfvanek/pg/checks/cluster/IntersectedForeignKeysCheckOnClusterTest.java @@ -0,0 +1,66 @@ +/* + * 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.FilterTablesByNamePredicate; +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.constraint.DuplicatedForeignKeys; +import io.github.mfvanek.pg.model.constraint.ForeignKey; +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 java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class IntersectedForeignKeysCheckOnClusterTest extends DatabaseAwareTestBase { + + private final DatabaseCheckOnCluster check = new IntersectedForeignKeysCheckOnCluster(getHaPgConnection()); + + @Test + void shouldSatisfyContract() { + assertThat(check.getType()).isEqualTo(DuplicatedForeignKeys.class); + assertThat(check.getDiagnostic()).isEqualTo(Diagnostic.INTERSECTED_FOREIGN_KEYS); + } + + @ParameterizedTest + @ValueSource(strings = {PgContext.DEFAULT_SCHEMA_NAME, "custom"}) + void shouldIgnoreCompletelyIdenticalForeignKeys(final String schemaName) { + executeTestOnDatabase(schemaName, dbp -> dbp.withReferences().withForeignKeyOnNullableColumn().withDuplicatedForeignKeys(), ctx -> + assertThat(check.check(ctx)) + .isEmpty()); + } + + @ParameterizedTest + @ValueSource(strings = {PgContext.DEFAULT_SCHEMA_NAME, "custom"}) + void onDatabaseWithThem(final String schemaName) { + executeTestOnDatabase(schemaName, dbp -> dbp.withReferences().withForeignKeyOnNullableColumn().withIntersectedForeignKeys(), ctx -> { + final String expectedTableName = ctx.enrichWithSchema("client_preferences"); + assertThat(check.check(ctx)) + .hasSize(1) + .containsExactly( + DuplicatedForeignKeys.of( + ForeignKey.of(expectedTableName, "c_client_preferences_email_phone_fk", + List.of(Column.ofNotNull(expectedTableName, "email"), Column.ofNotNull(expectedTableName, "phone"))), + ForeignKey.of(expectedTableName, "c_client_preferences_phone_email_fk", + List.of(Column.ofNotNull(expectedTableName, "phone"), Column.ofNotNull(expectedTableName, "email")))) + ); + + assertThat(check.check(ctx, FilterTablesByNamePredicate.of(expectedTableName))) + .isEmpty(); + }); + } +} diff --git a/spring-boot-integration/kotlin-demo-app/src/test/kotlin/io/github/mfvanek/pg/spring/postgres/kt/PostgresDemoApplicationKtTest.kt b/spring-boot-integration/kotlin-demo-app/src/test/kotlin/io/github/mfvanek/pg/spring/postgres/kt/PostgresDemoApplicationKtTest.kt index 26da8b70..31ceb6e4 100644 --- a/spring-boot-integration/kotlin-demo-app/src/test/kotlin/io/github/mfvanek/pg/spring/postgres/kt/PostgresDemoApplicationKtTest.kt +++ b/spring-boot-integration/kotlin-demo-app/src/test/kotlin/io/github/mfvanek/pg/spring/postgres/kt/PostgresDemoApplicationKtTest.kt @@ -11,7 +11,10 @@ package io.github.mfvanek.pg.spring.postgres.kt import com.zaxxer.hikari.HikariDataSource +import io.github.mfvanek.pg.common.maintenance.DatabaseCheckOnHost +import io.github.mfvanek.pg.common.maintenance.Diagnostic import io.github.mfvanek.pg.connection.PgConnection +import io.github.mfvanek.pg.model.DbObject import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired @@ -30,6 +33,9 @@ class PostgresDemoApplicationKtTest { @Autowired private lateinit var environment: Environment + @Autowired + private lateinit var checks: List> + @Test fun contextLoadsAndDoesNotContainPgIndexHealthBeans() { assertThat(applicationContext.getBean("dataSource")) @@ -43,4 +49,11 @@ class PostgresDemoApplicationKtTest { .startsWith("jdbc:postgresql://localhost:") .endsWith("/demo_for_pg_index_health_starter?loggerLevel=OFF") } + + @Test + fun checksShouldWork() { + assertThat(checks) + .hasSameSizeAs(Diagnostic.entries.toTypedArray()) + checks.forEach { c -> assertThat(c.check()).isEmpty() } + } } diff --git a/spring-boot-integration/pg-index-health-test-starter/src/main/java/io/github/mfvanek/pg/spring/DatabaseStructureHealthAutoConfiguration.java b/spring-boot-integration/pg-index-health-test-starter/src/main/java/io/github/mfvanek/pg/spring/DatabaseStructureHealthAutoConfiguration.java index f0ee2ade..b48bf74e 100644 --- a/spring-boot-integration/pg-index-health-test-starter/src/main/java/io/github/mfvanek/pg/spring/DatabaseStructureHealthAutoConfiguration.java +++ b/spring-boot-integration/pg-index-health-test-starter/src/main/java/io/github/mfvanek/pg/spring/DatabaseStructureHealthAutoConfiguration.java @@ -14,12 +14,14 @@ import io.github.mfvanek.pg.checks.host.ColumnsWithJsonTypeCheckOnHost; import io.github.mfvanek.pg.checks.host.ColumnsWithSerialTypesCheckOnHost; import io.github.mfvanek.pg.checks.host.ColumnsWithoutDescriptionCheckOnHost; +import io.github.mfvanek.pg.checks.host.DuplicatedForeignKeysCheckOnHost; import io.github.mfvanek.pg.checks.host.DuplicatedIndexesCheckOnHost; import io.github.mfvanek.pg.checks.host.ForeignKeysNotCoveredWithIndexCheckOnHost; import io.github.mfvanek.pg.checks.host.FunctionsWithoutDescriptionCheckOnHost; import io.github.mfvanek.pg.checks.host.IndexesWithBloatCheckOnHost; import io.github.mfvanek.pg.checks.host.IndexesWithBooleanCheckOnHost; import io.github.mfvanek.pg.checks.host.IndexesWithNullValuesCheckOnHost; +import io.github.mfvanek.pg.checks.host.IntersectedForeignKeysCheckOnHost; import io.github.mfvanek.pg.checks.host.IntersectedIndexesCheckOnHost; import io.github.mfvanek.pg.checks.host.InvalidIndexesCheckOnHost; import io.github.mfvanek.pg.checks.host.NotValidConstraintsCheckOnHost; @@ -257,6 +259,22 @@ public PrimaryKeysWithSerialTypesCheckOnHost primaryKeysWithSerialTypesCheckOnHo return new PrimaryKeysWithSerialTypesCheckOnHost(pgConnection); } + @Bean + @ConditionalOnClass(DuplicatedForeignKeysCheckOnHost.class) + @ConditionalOnBean(PgConnection.class) + @ConditionalOnMissingBean + public DuplicatedForeignKeysCheckOnHost duplicatedForeignKeysCheckOnHost(final PgConnection pgConnection) { + return new DuplicatedForeignKeysCheckOnHost(pgConnection); + } + + @Bean + @ConditionalOnClass(IntersectedForeignKeysCheckOnHost.class) + @ConditionalOnBean(PgConnection.class) + @ConditionalOnMissingBean + public IntersectedForeignKeysCheckOnHost intersectedForeignKeysCheckOnHost(final PgConnection pgConnection) { + return new IntersectedForeignKeysCheckOnHost(pgConnection); + } + @Bean @ConditionalOnClass(StatisticsMaintenanceOnHost.class) @ConditionalOnBean(PgConnection.class) diff --git a/spring-boot-integration/pg-index-health-test-starter/src/test/java/io/github/mfvanek/pg/spring/AutoConfigurationTestBase.java b/spring-boot-integration/pg-index-health-test-starter/src/test/java/io/github/mfvanek/pg/spring/AutoConfigurationTestBase.java index ba035238..93c912c1 100644 --- a/spring-boot-integration/pg-index-health-test-starter/src/test/java/io/github/mfvanek/pg/spring/AutoConfigurationTestBase.java +++ b/spring-boot-integration/pg-index-health-test-starter/src/test/java/io/github/mfvanek/pg/spring/AutoConfigurationTestBase.java @@ -52,7 +52,9 @@ abstract class AutoConfigurationTestBase { "statisticsMaintenanceOnHost", "configurationMaintenanceOnHost", "sequenceOverflowCheckOnHost", - "primaryKeysWithSerialTypesCheckOnHost" + "primaryKeysWithSerialTypesCheckOnHost", + "duplicatedForeignKeysCheckOnHost", + "intersectedForeignKeysCheckOnHost" ); protected static final Class[] EXPECTED_TYPES = {PgConnection.class, DatabaseCheckOnHost.class, StatisticsMaintenanceOnHost.class, ConfigurationMaintenanceOnHost.class}; protected static final DataSource DATA_SOURCE_MOCK = Mockito.mock(DataSource.class); diff --git a/spring-boot-integration/pg-index-health-test-starter/src/test/java/io/github/mfvanek/pg/spring/DatabaseStructureHealthAutoConfigurationFilteringTest.java b/spring-boot-integration/pg-index-health-test-starter/src/test/java/io/github/mfvanek/pg/spring/DatabaseStructureHealthAutoConfigurationFilteringTest.java index c520bff0..7e3e65f2 100644 --- a/spring-boot-integration/pg-index-health-test-starter/src/test/java/io/github/mfvanek/pg/spring/DatabaseStructureHealthAutoConfigurationFilteringTest.java +++ b/spring-boot-integration/pg-index-health-test-starter/src/test/java/io/github/mfvanek/pg/spring/DatabaseStructureHealthAutoConfigurationFilteringTest.java @@ -14,12 +14,14 @@ import io.github.mfvanek.pg.checks.host.ColumnsWithJsonTypeCheckOnHost; import io.github.mfvanek.pg.checks.host.ColumnsWithSerialTypesCheckOnHost; import io.github.mfvanek.pg.checks.host.ColumnsWithoutDescriptionCheckOnHost; +import io.github.mfvanek.pg.checks.host.DuplicatedForeignKeysCheckOnHost; import io.github.mfvanek.pg.checks.host.DuplicatedIndexesCheckOnHost; import io.github.mfvanek.pg.checks.host.ForeignKeysNotCoveredWithIndexCheckOnHost; import io.github.mfvanek.pg.checks.host.FunctionsWithoutDescriptionCheckOnHost; import io.github.mfvanek.pg.checks.host.IndexesWithBloatCheckOnHost; import io.github.mfvanek.pg.checks.host.IndexesWithBooleanCheckOnHost; import io.github.mfvanek.pg.checks.host.IndexesWithNullValuesCheckOnHost; +import io.github.mfvanek.pg.checks.host.IntersectedForeignKeysCheckOnHost; import io.github.mfvanek.pg.checks.host.IntersectedIndexesCheckOnHost; import io.github.mfvanek.pg.checks.host.InvalidIndexesCheckOnHost; import io.github.mfvanek.pg.checks.host.NotValidConstraintsCheckOnHost; @@ -114,7 +116,9 @@ private static List> getCheckTypes() { StatisticsMaintenanceOnHost.class, ConfigurationMaintenanceOnHost.class, SequenceOverflowCheckOnHost.class, - PrimaryKeysWithSerialTypesCheckOnHost.class + PrimaryKeysWithSerialTypesCheckOnHost.class, + DuplicatedForeignKeysCheckOnHost.class, + IntersectedForeignKeysCheckOnHost.class ); } } diff --git a/spring-boot-integration/postgres-demo-app/src/test/java/io/github/mfvanek/pg/spring/postgres/PostgresDemoApplicationTest.java b/spring-boot-integration/postgres-demo-app/src/test/java/io/github/mfvanek/pg/spring/postgres/PostgresDemoApplicationTest.java index 912a66c1..705b7ecd 100644 --- a/spring-boot-integration/postgres-demo-app/src/test/java/io/github/mfvanek/pg/spring/postgres/PostgresDemoApplicationTest.java +++ b/spring-boot-integration/postgres-demo-app/src/test/java/io/github/mfvanek/pg/spring/postgres/PostgresDemoApplicationTest.java @@ -11,7 +11,10 @@ package io.github.mfvanek.pg.spring.postgres; import com.zaxxer.hikari.HikariDataSource; +import io.github.mfvanek.pg.common.maintenance.DatabaseCheckOnHost; +import io.github.mfvanek.pg.common.maintenance.Diagnostic; import io.github.mfvanek.pg.connection.PgConnection; +import io.github.mfvanek.pg.model.DbObject; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @@ -19,6 +22,8 @@ import org.springframework.core.env.Environment; import org.springframework.test.context.ActiveProfiles; +import java.util.List; + import static org.assertj.core.api.Assertions.assertThat; @ActiveProfiles("test") @@ -31,6 +36,9 @@ class PostgresDemoApplicationTest { @Autowired private Environment environment; + @Autowired + private List> checks; + @Test void contextLoadsAndDoesNotContainPgIndexHealthBeans() { assertThat(applicationContext.getBean("dataSource")) @@ -44,4 +52,11 @@ void contextLoadsAndDoesNotContainPgIndexHealthBeans() { .startsWith("jdbc:postgresql://localhost:") .endsWith("/demo_for_pg_index_health_starter?loggerLevel=OFF"); } + + @Test + void checksShouldWork() { + assertThat(checks) + .hasSameSizeAs(Diagnostic.values()); + checks.forEach(c -> assertThat(c.check()).isEmpty()); + } }