Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[POSSIBLE_OBJECT_NAME_OVERFLOW] Add support for partitioned tables. [PRIMARY_KEYS_WITH_SERIAL_TYPES] Add support for partitioned tables. [PRIMARY_KEYS_WITH_SERIAL_TYPES] Fix insufficient permissions error #544

Merged
merged 4 commits into from
Dec 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,10 @@ All checks can be divided into 2 groups:
| 17 | Tables with [not valid constraints](https://habr.com/ru/articles/800121/) | **runtime**/static | no | [sql](https://github.com/mfvanek/pg-index-health-sql/blob/master/sql/not_valid_constraints.sql) |
| 18 | B-tree indexes [on array columns](https://habr.com/ru/articles/800121/) | static | no | [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** | not applicable | [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 | no | [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 | yes | [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 | no | [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 | no | [sql](https://github.com/mfvanek/pg-index-health-sql/blob/master/sql/intersected_foreign_keys.sql) |
| 23 | Possible object name overflow (identifiers with maximum length) | static | no | [sql](https://github.com/mfvanek/pg-index-health-sql/blob/master/sql/possible_object_name_overflow.sql) |
| 23 | Possible object name overflow (identifiers with maximum length) | static | yes | [sql](https://github.com/mfvanek/pg-index-health-sql/blob/master/sql/possible_object_name_overflow.sql) |
| 24 | Tables not linked to other tables | static | yes | [sql](https://github.com/mfvanek/pg-index-health-sql/blob/master/sql/tables_not_linked_to_others.sql) |
| 25 | Foreign keys [with unmatched column type](https://habr.com/ru/articles/803841/) | static | no | [sql](https://github.com/mfvanek/pg-index-health-sql/blob/master/sql/foreign_keys_with_unmatched_column_type.sql) |

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.dbobject.AnyObject;
import io.github.mfvanek.pg.model.dbobject.PgObjectType;
Expand Down Expand Up @@ -42,18 +43,38 @@ void shouldSatisfyContract() {
@ValueSource(strings = {PgContext.DEFAULT_SCHEMA_NAME, "custom"})
void onDatabaseWithThem(final String schemaName) {
executeTestOnDatabase(schemaName, dbp -> dbp.withReferences().withMaterializedView().withIdentityPrimaryKey(), ctx -> {
final String matViewName = ctx.enrichWithSchema("accounts_materialized_view_with_length_63_1234567890_1234567890");
final String constraintName = ctx.enrichWithSchema("num_less_than_million_constraint_with_length_63_1234567890_1234");
final String matViewName = "accounts_materialized_view_with_length_63_1234567890_1234567890";
final String constraintName = "num_less_than_million_constraint_with_length_63_1234567890_1234";
assertThat(check)
.executing(ctx)
.hasSize(2)
.containsExactlyInAnyOrder(
AnyObject.ofType(matViewName, PgObjectType.MATERIALIZED_VIEW),
AnyObject.ofType(constraintName, PgObjectType.CONSTRAINT));
AnyObject.ofType(ctx, matViewName, PgObjectType.MATERIALIZED_VIEW),
AnyObject.ofType(ctx, constraintName, PgObjectType.CONSTRAINT));

assertThat(check)
.executing(ctx, SkipDbObjectsByNamePredicate.of(List.of(matViewName, constraintName)))
.executing(ctx, SkipDbObjectsByNamePredicate.of(ctx, List.of(matViewName, constraintName)))
.isEmpty();
});
}

@ParameterizedTest
@ValueSource(strings = {PgContext.DEFAULT_SCHEMA_NAME, "custom"})
void shouldWorkWithPartitionedTables(final String schemaName) {
executeTestOnDatabase(schemaName, DatabasePopulator::withVeryLongNamesInPartitionedTable, ctx ->
assertThat(check)
.executing(ctx)
.hasSize(9)
.containsExactly(
AnyObject.ofType(ctx, "entity_default_long_1234567890_1234567890_1234567890_12345_pkey", PgObjectType.CONSTRAINT),
AnyObject.ofType(ctx, "entity_long_1234567890_1234567890_1234567890_1234567890_12_pkey", PgObjectType.CONSTRAINT),
AnyObject.ofType(ctx, "entity_default_long_1234567890_1234567890_1234567890_12345_pkey", PgObjectType.INDEX),
AnyObject.ofType(ctx, "entity_default_long_1234567890_123456789_ref_type_ref_value_idx", PgObjectType.INDEX),
AnyObject.ofType(ctx, "entity_long_1234567890_1234567890_1234567890_1234567890_12_pkey", PgObjectType.PARTITIONED_INDEX),
AnyObject.ofType(ctx, "idx_entity_long_1234567890_1234567890_1234567890_1234567890_123", PgObjectType.PARTITIONED_INDEX),
AnyObject.ofType(ctx, "entity_long_1234567890_1234567890_1234567890_1234567890_1234567", PgObjectType.PARTITIONED_TABLE),
AnyObject.ofType(ctx, "entity_long_1234567890_1234567890_1234567890_1234_entity_id_seq", PgObjectType.SEQUENCE),
AnyObject.ofType(ctx, "entity_default_long_1234567890_1234567890_1234567890_1234567890", PgObjectType.TABLE)
));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.column.Column;
import io.github.mfvanek.pg.model.column.ColumnWithSerialType;
import io.github.mfvanek.pg.model.column.SerialType;
Expand Down Expand Up @@ -69,4 +70,19 @@ void onDatabaseWithoutThem(final String schemaName) {
.isEmpty()
);
}

@ParameterizedTest
@ValueSource(strings = {PgContext.DEFAULT_SCHEMA_NAME, "custom"})
void shouldWorkWithPartitionedTables(final String schemaName) {
executeTestOnDatabase(schemaName, DatabasePopulator::withVeryLongNamesInPartitionedTable, ctx -> {
final String tableName = "entity_long_1234567890_1234567890_1234567890_1234567890_1234567";
final String sequenceName = ctx.enrichWithSchema("entity_long_1234567890_1234567890_1234567890_1234_entity_id_seq");
assertThat(check)
.executing(ctx)
.hasSize(1)
.containsExactly(
ColumnWithSerialType.of(Column.ofNotNull(ctx, tableName, "entity_id"), SerialType.BIG_SERIAL, sequenceName)
);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
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.CreatePartitionedTableWithVeryLongNamesStatement;
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;
Expand Down Expand Up @@ -337,6 +338,12 @@ public DatabasePopulator withNullableIndexesInPartitionedTable() {
return this;
}

@Nonnull
public DatabasePopulator withVeryLongNamesInPartitionedTable() {
statementsToExecuteInSameTransaction.putIfAbsent(114, new CreatePartitionedTableWithVeryLongNamesStatement());
return this;
}

public void populate() {
try (SchemaNameHolder ignored = SchemaNameHolder.with(schemaName)) {
ExecuteUtils.executeInTransaction(dataSource, statementsToExecuteInSameTransaction.values());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* 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 CreatePartitionedTableWithVeryLongNamesStatement extends AbstractDbStatement {

@Nonnull
@Override
protected List<String> getSqlToExecute() {
return List.of(
"create table if not exists {schemaName}.entity_long_1234567890_1234567890_1234567890_1234567890_1234567(" +
"ref_type varchar(32)," +
"ref_value varchar(64)," +
"entity_id bigserial primary key" +
") partition by range (entity_id);",
"create index if not exists idx_entity_long_1234567890_1234567890_1234567890_1234567890_123 " +
"on {schemaName}.entity_long_1234567890_1234567890_1234567890_1234567890_1234567 (ref_type, ref_value);",
"create table if not exists {schemaName}.entity_default_long_1234567890_1234567890_1234567890_1234567890 " +
"partition of {schemaName}.entity_long_1234567890_1234567890_1234567890_1234567890_1234567 default;"
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

package io.github.mfvanek.pg.model.dbobject;

import io.github.mfvanek.pg.model.context.PgContext;
import io.github.mfvanek.pg.model.validation.Validators;

import java.util.Objects;
Expand Down Expand Up @@ -108,18 +109,35 @@ public int compareTo(@Nonnull final AnyObject other) {
* @param objectType type of object in a database; should be non-null.
* @return {@code AnyObject} instance
*/
public static AnyObject ofType(@Nonnull final String objectName, @Nonnull final PgObjectType objectType) {
public static AnyObject ofType(@Nonnull final String objectName,
@Nonnull final PgObjectType objectType) {
return new AnyObject(objectName, objectType);
}

/**
* Constructs an {@code AnyObject} instance with given context.
*
* @param pgContext the schema context to enrich object name; must be non-null.
* @param objectName name of object in a database; should be non-blank.
* @param objectType type of object in a database; should be non-null.
* @return {@code AnyObject} instance
* @since 0.14.4
*/
public static AnyObject ofType(@Nonnull final PgContext pgContext,
@Nonnull final String objectName,
@Nonnull final PgObjectType objectType) {
return ofType(PgContext.enrichWith(objectName, pgContext), objectType);
}

/**
* Constructs an {@code AnyObject} instance.
*
* @param objectName name of object in a database; should be non-blank.
* @param objectType literal type of object in a database; should be non-null.
* @return {@code AnyObject} instance
*/
public static AnyObject ofRaw(@Nonnull final String objectName, @Nonnull final String objectType) {
public static AnyObject ofRaw(@Nonnull final String objectName,
@Nonnull final String objectType) {
return new AnyObject(objectName, PgObjectType.valueFrom(objectType));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,26 @@ public enum PgObjectType {
* @see <a href="https://www.postgresql.org/docs/current/sql-createtable.html">CREATE TABLE</a>
*/
TABLE("table"),
/**
* A partitioned table in a database.
*
* @see <a href="https://www.postgresql.org/docs/current/ddl-partitioning.html">Table Partitioning</a>
* @since 0.14.4
*/
PARTITIONED_TABLE("partitioned table"),
/**
* An index in a database.
*
* @see <a href="https://www.postgresql.org/docs/current/indexes-types.html">Index Types</a>
*/
INDEX("index"),
/**
* A partitioned index in a database.
*
* @see <a href="https://www.postgresql.org/docs/current/ddl-partitioning.html">Table Partitioning</a>
* @since 0.14.4
*/
PARTITIONED_INDEX("partitioned index"),
/**
* A sequence in a database.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

package io.github.mfvanek.pg.model.predicates;

import io.github.mfvanek.pg.model.context.PgContext;
import io.github.mfvanek.pg.model.dbobject.DbObject;

import java.util.Collection;
Expand All @@ -22,6 +23,8 @@
import javax.annotation.concurrent.Immutable;
import javax.annotation.concurrent.ThreadSafe;

import static io.github.mfvanek.pg.model.predicates.AbstractSkipTablesPredicate.prepareFullyQualifiedNamesToSkip;

/**
* A predicate for filtering database objects by their fully qualified names.
* <p>
Expand Down Expand Up @@ -86,6 +89,20 @@ public static Predicate<DbObject> ofName(@Nonnull final String fullyQualifiedObj
return new SkipDbObjectsByNamePredicate(fullyQualifiedObjectNameToSkip);
}

/**
* Creates a predicate to skip a specific object name in the given context.
*
* @param pgContext the PostgreSQL context to use; must be non-null
* @param objectNameToSkip the object name to skip, must be non-null and non-blank
* @return a predicate that skips the specified object
* @since 0.14.4
*/
@Nonnull
public static Predicate<DbObject> ofName(@Nonnull final PgContext pgContext,
@Nonnull final String objectNameToSkip) {
return ofName(PgContext.enrichWith(objectNameToSkip, pgContext));
}

/**
* Creates a predicate to skip multiple fully qualified object names.
*
Expand All @@ -97,4 +114,18 @@ public static Predicate<DbObject> ofName(@Nonnull final String fullyQualifiedObj
public static Predicate<DbObject> of(@Nonnull final Collection<String> fullyQualifiedObjectNamesToSkip) {
return new SkipDbObjectsByNamePredicate(fullyQualifiedObjectNamesToSkip);
}

/**
* Creates a predicate to skip multiple object names in the given context.
*
* @param pgContext the PostgreSQL context to use; must be non-null
* @param objectNamesToSkip a collection of object names to skip, must be non-null
* @return a predicate that skips the specified objects
* @since 0.14.4
*/
@Nonnull
public static Predicate<DbObject> of(@Nonnull final PgContext pgContext,
@Nonnull final Collection<String> objectNamesToSkip) {
return of(prepareFullyQualifiedNamesToSkip(pgContext, objectNamesToSkip));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

package io.github.mfvanek.pg.model.dbobject;

import io.github.mfvanek.pg.model.context.PgContext;
import nl.jqno.equalsverifier.EqualsVerifier;
import org.junit.jupiter.api.Test;

Expand Down Expand Up @@ -58,6 +59,10 @@ void withInvalidValues() {
void testToString() {
assertThat(AnyObject.ofRaw("mv", "Materialized View"))
.hasToString("AnyObject{objectName='mv', objectType=MATERIALIZED_VIEW}");
assertThat(AnyObject.ofType("pi", PgObjectType.PARTITIONED_INDEX))
.hasToString("AnyObject{objectName='pi', objectType=PARTITIONED_INDEX}");
assertThat(AnyObject.ofType(PgContext.of("tst"), "pi", PgObjectType.PARTITIONED_INDEX))
.hasToString("AnyObject{objectName='tst.pi', objectType=PARTITIONED_INDEX}");
}

@SuppressWarnings("ConstantConditions")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

package io.github.mfvanek.pg.model.predicates;

import io.github.mfvanek.pg.model.context.PgContext;
import io.github.mfvanek.pg.model.table.Table;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
Expand All @@ -21,9 +22,10 @@

class SkipDbObjectsByNamePredicateTest {

private static final Table FIRST = Table.of("custom.TABLE1", 1L);
private static final Table SECOND = Table.of("custom.TABLE2", 2L);
private static final Table THIRD = Table.of("custom.TABLE3", 3L);
private static final PgContext CTX = PgContext.of("CUSTOM");
private static final Table FIRST = Table.of(CTX, "TABLE1", 1L);
private static final Table SECOND = Table.of(CTX, "TABLE2", 2L);
private static final Table THIRD = Table.of(CTX, "TABLE3", 3L);

@SuppressWarnings("DataFlowIssue")
@Test
Expand All @@ -43,6 +45,9 @@ void shouldThrowExceptionWhenInvalidDataPassed() {
assertThatThrownBy(() -> SkipDbObjectsByNamePredicate.of(null))
.isInstanceOf(NullPointerException.class)
.hasMessage("fullyQualifiedObjectNamesToSkip cannot be null");
assertThatThrownBy(() -> SkipDbObjectsByNamePredicate.of(null, null))
.isInstanceOf(NullPointerException.class)
.hasMessage("pgContext cannot be null");
}

@Test
Expand All @@ -51,6 +56,11 @@ void caseShouldNotMatter() {
.accepts(FIRST)
.accepts(SECOND)
.rejects(THIRD);

assertThat(SkipDbObjectsByNamePredicate.ofName(CTX, "table3"))
.accepts(FIRST)
.accepts(SECOND)
.rejects(THIRD);
}

@Test
Expand All @@ -59,6 +69,11 @@ void forList() {
.rejects(FIRST)
.accepts(SECOND)
.rejects(THIRD);

assertThat(SkipDbObjectsByNamePredicate.of(CTX, List.of("table1", "table3")))
.rejects(FIRST)
.accepts(SECOND)
.rejects(THIRD);
}

@Test
Expand Down
Loading
Loading