From 5709af1fe1502263d164e3ad21cecd160d012a91 Mon Sep 17 00:00:00 2001 From: Ivan Vakhrushev Date: Thu, 12 Dec 2024 00:01:44 +0400 Subject: [PATCH] [INDEXES_WITH_NULL_VALUES] Enhance support for partitioned tables/indexes (#540) * Update queries * [INDEXES_WITH_NULL_VALUES] Enhance support for partitioned tables/indexes * Fix checkstyle --- README.md | 4 +- pg-index-health-core/src/main/resources | 2 +- .../IndexesWithNullValuesCheckOnHostTest.java | 16 +++++++- .../fixtures/support/DatabasePopulator.java | 7 ++++ ...ionedTableWithNullableFieldsStatement.java | 38 +++++++++++++++++++ .../pg/model/index/IndexWithNulls.java | 18 +++++++++ .../pg/model/index/IndexWithNullsTest.java | 2 + ...dexesWithNullValuesCheckOnClusterTest.java | 16 +++++++- 8 files changed, 98 insertions(+), 5 deletions(-) create mode 100644 pg-index-health-core/src/testFixtures/java/io/github/mfvanek/pg/core/fixtures/support/statements/CreatePartitionedTableWithNullableFieldsStatement.java diff --git a/README.md b/README.md index 86395ea9..809d47e2 100644 --- a/README.md +++ b/README.md @@ -48,12 +48,12 @@ All checks can be divided into 2 groups: | № | Description | Type | Supports partitioning | SQL query | |----|-------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------|-----------------------|-------------------------------------------------------------------------------------------------------------------| -| 1 | Invalid (broken) indexes | **runtime**/static | no | [sql](https://github.com/mfvanek/pg-index-health-sql/blob/master/sql/invalid_indexes.sql) | +| 1 | Invalid (broken) indexes | **runtime**/static | yes | [sql](https://github.com/mfvanek/pg-index-health-sql/blob/master/sql/invalid_indexes.sql) | | 1 | Duplicated (completely identical) indexes | static | no | [sql](https://github.com/mfvanek/pg-index-health-sql/blob/master/sql/duplicated_indexes.sql) | | 3 | Intersected (partially identical) indexes | static | no | [sql](https://github.com/mfvanek/pg-index-health-sql/blob/master/sql/intersected_indexes.sql) | | 4 | Unused indexes | **runtime** | no | [sql](https://github.com/mfvanek/pg-index-health-sql/blob/master/sql/unused_indexes.sql) | | 5 | Foreign keys without associated indexes | static | no | [sql](https://github.com/mfvanek/pg-index-health-sql/blob/master/sql/foreign_keys_without_index.sql) | -| 6 | Indexes with null values | static | no | [sql](https://github.com/mfvanek/pg-index-health-sql/blob/master/sql/indexes_with_null_values.sql) | +| 6 | Indexes with null values | static | yes | [sql](https://github.com/mfvanek/pg-index-health-sql/blob/master/sql/indexes_with_null_values.sql) | | 7 | Tables with missing indexes | **runtime** | yes | [sql](https://github.com/mfvanek/pg-index-health-sql/blob/master/sql/tables_with_missing_indexes.sql) | | 8 | Tables without primary key | static | yes | [sql](https://github.com/mfvanek/pg-index-health-sql/blob/master/sql/tables_without_primary_key.sql) | | 9 | Indexes [bloat](https://www.percona.com/blog/2018/08/06/basic-understanding-bloat-vacuum-postgresql-mvcc/) | **runtime** | no | [sql](https://github.com/mfvanek/pg-index-health-sql/blob/master/sql/bloated_indexes.sql) | diff --git a/pg-index-health-core/src/main/resources b/pg-index-health-core/src/main/resources index e9b0afef..eebce6a5 160000 --- a/pg-index-health-core/src/main/resources +++ b/pg-index-health-core/src/main/resources @@ -1 +1 @@ -Subproject commit e9b0afef7bf61f614122aeabe16a357158c4303a +Subproject commit eebce6a5365f83266db58058c9a6966d2d8c2381 diff --git a/pg-index-health-core/src/test/java/io/github/mfvanek/pg/core/checks/host/IndexesWithNullValuesCheckOnHostTest.java b/pg-index-health-core/src/test/java/io/github/mfvanek/pg/core/checks/host/IndexesWithNullValuesCheckOnHostTest.java index 76353270..a760fb6b 100644 --- a/pg-index-health-core/src/test/java/io/github/mfvanek/pg/core/checks/host/IndexesWithNullValuesCheckOnHostTest.java +++ b/pg-index-health-core/src/test/java/io/github/mfvanek/pg/core/checks/host/IndexesWithNullValuesCheckOnHostTest.java @@ -13,6 +13,7 @@ import io.github.mfvanek.pg.core.checks.common.DatabaseCheckOnHost; import io.github.mfvanek.pg.core.checks.common.Diagnostic; import io.github.mfvanek.pg.core.fixtures.support.DatabaseAwareTestBase; +import io.github.mfvanek.pg.core.fixtures.support.DatabasePopulator; import io.github.mfvanek.pg.model.context.PgContext; import io.github.mfvanek.pg.model.index.IndexWithNulls; import io.github.mfvanek.pg.model.predicates.SkipIndexesByNamePredicate; @@ -43,7 +44,7 @@ void onDatabaseWithThem(final String schemaName) { assertThat(check) .executing(ctx) .hasSize(1) - .containsExactly(IndexWithNulls.of(ctx, "clients", "i_clients_middle_name", 0L, "middle_name")) + .containsExactly(IndexWithNulls.of(ctx, "clients", "i_clients_middle_name", "middle_name")) .allMatch(i -> i.getNullableColumn().isNullable()); assertThat(check) @@ -55,4 +56,17 @@ void onDatabaseWithThem(final String schemaName) { .isEmpty(); }); } + + @ParameterizedTest + @ValueSource(strings = {PgContext.DEFAULT_SCHEMA_NAME, "custom"}) + void shouldWorkWithPartitionedTables(final String schemaName) { + executeTestOnDatabase(schemaName, DatabasePopulator::withNullableIndexesInPartitionedTable, ctx -> + assertThat(check) + .executing(ctx) + .hasSize(2) + .containsExactly( + IndexWithNulls.of(ctx, "custom_entity_reference_with_very_very_very_long_name", "idx_custom_entity_reference_with_very_very_very_long_name_1", "ref_type"), + IndexWithNulls.of(ctx, "custom_entity_reference_with_very_very_very_long_name_1_default", "idx_custom_entity_reference_with_very_very_very_long_name_1_d_3", "ref_type")) + ); + } } diff --git a/pg-index-health-core/src/testFixtures/java/io/github/mfvanek/pg/core/fixtures/support/DatabasePopulator.java b/pg-index-health-core/src/testFixtures/java/io/github/mfvanek/pg/core/fixtures/support/DatabasePopulator.java index 78cb427c..157340b8 100644 --- a/pg-index-health-core/src/testFixtures/java/io/github/mfvanek/pg/core/fixtures/support/DatabasePopulator.java +++ b/pg-index-health-core/src/testFixtures/java/io/github/mfvanek/pg/core/fixtures/support/DatabasePopulator.java @@ -37,6 +37,7 @@ import io.github.mfvanek.pg.core.fixtures.support.statements.CreateIndexesWithDifferentOpclassStatement; import io.github.mfvanek.pg.core.fixtures.support.statements.CreateMaterializedViewStatement; import io.github.mfvanek.pg.core.fixtures.support.statements.CreateNotSuitableIndexForForeignKeyStatement; +import io.github.mfvanek.pg.core.fixtures.support.statements.CreatePartitionedTableWithNullableFieldsStatement; import io.github.mfvanek.pg.core.fixtures.support.statements.CreatePartitionedTableWithoutCommentsStatement; import io.github.mfvanek.pg.core.fixtures.support.statements.CreatePartitionedTableWithoutPrimaryKeyStatement; import io.github.mfvanek.pg.core.fixtures.support.statements.CreateProceduresStatement; @@ -330,6 +331,12 @@ public DatabasePopulator withPrimaryKeyForDefaultPartition() { return this; } + @Nonnull + public DatabasePopulator withNullableIndexesInPartitionedTable() { + statementsToExecuteInSameTransaction.putIfAbsent(113, new CreatePartitionedTableWithNullableFieldsStatement()); + 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/core/fixtures/support/statements/CreatePartitionedTableWithNullableFieldsStatement.java b/pg-index-health-core/src/testFixtures/java/io/github/mfvanek/pg/core/fixtures/support/statements/CreatePartitionedTableWithNullableFieldsStatement.java new file mode 100644 index 00000000..85896df6 --- /dev/null +++ b/pg-index-health-core/src/testFixtures/java/io/github/mfvanek/pg/core/fixtures/support/statements/CreatePartitionedTableWithNullableFieldsStatement.java @@ -0,0 +1,38 @@ +/* + * 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.core.fixtures.support.statements; + +import java.util.List; +import javax.annotation.Nonnull; + +public class CreatePartitionedTableWithNullableFieldsStatement extends AbstractDbStatement { + + @Nonnull + @Override + protected List getSqlToExecute() { + return List.of( + "create table if not exists {schemaName}.custom_entity_reference_with_very_very_very_long_name(" + + "ref_type varchar(32)," + + "ref_value varchar(64)," + + "creation_date timestamp with time zone not null," + + "entity_id varchar(64) not null" + + ") partition by range (creation_date);", + "create index if not exists idx_custom_entity_reference_with_very_very_very_long_name_1 " + + "on {schemaName}.custom_entity_reference_with_very_very_very_long_name (ref_type, ref_value);", + "create index if not exists idx_custom_entity_reference_with_very_very_very_long_name_2 " + + "on {schemaName}.custom_entity_reference_with_very_very_very_long_name (entity_id, ref_value);", + "create table if not exists {schemaName}.custom_entity_reference_with_very_very_very_long_name_1_default " + + "partition of {schemaName}.custom_entity_reference_with_very_very_very_long_name default;", + "create index if not exists idx_custom_entity_reference_with_very_very_very_long_name_1_d_3 " + + "on {schemaName}.custom_entity_reference_with_very_very_very_long_name_1_default (ref_type, ref_value);" + ); + } +} 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 e948e961..27e2fb98 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 @@ -90,4 +90,22 @@ public static IndexWithNulls of(@Nonnull final PgContext pgContext, return new IndexWithNulls(PgContext.enrichWith(tableName, pgContext), PgContext.enrichWith(indexName, pgContext), indexSizeInBytes, Column.ofNullable(pgContext, tableName, nullableColumnName)); } + + /** + * Constructs an {@code IndexWithNulls} object with given context. + * + * @param pgContext the schema context to enrich table and index name; must be non-null. + * @param tableName table name; should be non-blank. + * @param indexName index name; should be non-blank. + * @param nullableColumnName nullable column in this index. + * @return {@code IndexWithNulls} + * @since 0.14.3 + */ + @Nonnull + public static IndexWithNulls of(@Nonnull final PgContext pgContext, + @Nonnull final String tableName, + @Nonnull final String indexName, + @Nonnull final String nullableColumnName) { + return of(pgContext, tableName, indexName, 0L, nullableColumnName); + } } 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 70b8bef7..b75871b7 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 @@ -74,6 +74,8 @@ void testToString() { .hasToString("IndexWithNulls{tableName='t', indexName='i', indexSizeInBytes=22, columns=[Column{tableName='t', columnName='f', notNull=false}]}"); assertThat(IndexWithNulls.of(PgContext.of("tst"), "t", "i", 22L, "f")) .hasToString("IndexWithNulls{tableName='tst.t', indexName='tst.i', indexSizeInBytes=22, columns=[Column{tableName='tst.t', columnName='f', notNull=false}]}"); + assertThat(IndexWithNulls.of(PgContext.of("tst"), "t", "i", "f")) + .hasToString("IndexWithNulls{tableName='tst.t', indexName='tst.i', indexSizeInBytes=0, columns=[Column{tableName='tst.t', columnName='f', notNull=false}]}"); } @SuppressWarnings("ConstantConditions") diff --git a/pg-index-health/src/test/java/io/github/mfvanek/pg/health/checks/cluster/IndexesWithNullValuesCheckOnClusterTest.java b/pg-index-health/src/test/java/io/github/mfvanek/pg/health/checks/cluster/IndexesWithNullValuesCheckOnClusterTest.java index 634bf11f..55b3a994 100644 --- a/pg-index-health/src/test/java/io/github/mfvanek/pg/health/checks/cluster/IndexesWithNullValuesCheckOnClusterTest.java +++ b/pg-index-health/src/test/java/io/github/mfvanek/pg/health/checks/cluster/IndexesWithNullValuesCheckOnClusterTest.java @@ -12,6 +12,7 @@ import io.github.mfvanek.pg.core.checks.common.Diagnostic; import io.github.mfvanek.pg.core.fixtures.support.DatabaseAwareTestBase; +import io.github.mfvanek.pg.core.fixtures.support.DatabasePopulator; import io.github.mfvanek.pg.health.checks.common.DatabaseCheckOnCluster; import io.github.mfvanek.pg.model.context.PgContext; import io.github.mfvanek.pg.model.index.IndexWithNulls; @@ -51,7 +52,7 @@ void onDatabaseWithThem(final String schemaName) { assertThat(check) .executing(ctx) .hasSize(1) - .containsExactly(IndexWithNulls.of(ctx, "clients", "i_clients_middle_name", 0L, "middle_name")) + .containsExactly(IndexWithNulls.of(ctx, "clients", "i_clients_middle_name", "middle_name")) .allMatch(i -> i.getNullableColumn().isNullable()); assertThat(check) @@ -63,4 +64,17 @@ void onDatabaseWithThem(final String schemaName) { .isEmpty(); }); } + + @ParameterizedTest + @ValueSource(strings = {PgContext.DEFAULT_SCHEMA_NAME, "custom"}) + void shouldWorkWithPartitionedTables(final String schemaName) { + executeTestOnDatabase(schemaName, DatabasePopulator::withNullableIndexesInPartitionedTable, ctx -> + assertThat(check) + .executing(ctx) + .hasSize(2) + .containsExactly( + IndexWithNulls.of(ctx, "custom_entity_reference_with_very_very_very_long_name", "idx_custom_entity_reference_with_very_very_very_long_name_1", "ref_type"), + IndexWithNulls.of(ctx, "custom_entity_reference_with_very_very_very_long_name_1_default", "idx_custom_entity_reference_with_very_very_very_long_name_1_d_3", "ref_type")) + ); + } }