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

Issue 207 #269

Merged
merged 19 commits into from
Nov 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
6 changes: 5 additions & 1 deletion config/spotbugs/exclude.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<FindBugsFilter>
<Match>
<Bug pattern="EI_EXPOSE_REP,RCN_REDUNDANT_NULLCHECK_WOULD_HAVE_BEEN_A_NPE,SQL_NONCONSTANT_STRING_PASSED_TO_EXECUTE,BC_UNCONFIRMED_CAST_OF_RETURN_VALUE"/>
<Bug pattern="EI_EXPOSE_REP,EI_EXPOSE_REP2,RCN_REDUNDANT_NULLCHECK_WOULD_HAVE_BEEN_A_NPE,SQL_NONCONSTANT_STRING_PASSED_TO_EXECUTE,BC_UNCONFIRMED_CAST_OF_RETURN_VALUE"/>
</Match>
<Match>
<Bug pattern="RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE,UWF_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR"/>
Expand All @@ -15,6 +15,10 @@
<Bug pattern="SIL_SQL_IN_LOOP"/>
<Class name="io.github.mfvanek.pg.index.health.demo.utils.MigrationsGenerator"/>
</Match>
<Match>
<Bug pattern="PRMC_POSSIBLY_REDUNDANT_METHOD_CALLS,SIL_SQL_IN_LOOP"/>
mfvanek marked this conversation as resolved.
Show resolved Hide resolved
<Class name="io.github.mfvanek.pg.index.health.demo.service.DbMigrationGeneratorService"/>
</Match>
<Match>
<Bug pattern="CRLF_INJECTION_LOGS,SQL_INJECTION_JDBC"/>
</Match>
Expand Down
1 change: 1 addition & 0 deletions pg-index-health-spring-boot-demo/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ dependencies {
implementation("org.testcontainers:postgresql")
implementation("io.github.mfvanek:pg-index-health")
implementation("io.github.mfvanek:pg-index-health-logger")
implementation("io.github.mfvanek:pg-index-health-generator")
implementation("com.github.blagerweij:liquibase-sessionlock:1.6.9")

annotationProcessor(platform("org.springframework.boot:spring-boot-dependencies:3.3.5"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import io.github.mfvanek.pg.connection.HighAvailabilityPgConnectionFactoryImpl;
import io.github.mfvanek.pg.connection.PgConnectionFactoryImpl;
import io.github.mfvanek.pg.connection.PrimaryHostDeterminerImpl;
import io.github.mfvanek.pg.model.PgContext;
import io.github.mfvanek.pg.settings.maintenance.ConfigurationMaintenanceOnHostImpl;
import io.github.mfvanek.pg.statistics.maintenance.StatisticsMaintenanceOnHostImpl;
import org.springframework.context.annotation.Bean;
Expand Down Expand Up @@ -59,4 +60,9 @@ public DatabaseManagement databaseManagement(@Nonnull final HighAvailabilityPgCo
StatisticsMaintenanceOnHostImpl::new,
ConfigurationMaintenanceOnHostImpl::new);
}

@Bean
public PgContext pgContext() {
return PgContext.of("demo");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright (c) 2019-2024. Ivan Vakhrushev and others.
* https://github.com/mfvanek/pg-index-health-demo
*
* Licensed under the Apache License 2.0
*/

package io.github.mfvanek.pg.index.health.demo.config;

import io.github.mfvanek.pg.checks.cluster.ForeignKeysNotCoveredWithIndexCheckOnCluster;
import io.github.mfvanek.pg.common.maintenance.DatabaseCheckOnCluster;
import io.github.mfvanek.pg.connection.HighAvailabilityPgConnection;
import io.github.mfvanek.pg.generator.DbMigrationGenerator;
import io.github.mfvanek.pg.generator.ForeignKeyMigrationGenerator;
import io.github.mfvanek.pg.generator.GeneratingOptions;
import io.github.mfvanek.pg.model.constraint.ForeignKey;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration(proxyBeanMethods = false)
public class MigrationGeneratorConfig {

@Bean
public DbMigrationGenerator<ForeignKey> dbMigrationGenerator() {
return new ForeignKeyMigrationGenerator(GeneratingOptions.builder().build());
}

@Bean
public DatabaseCheckOnCluster<ForeignKey> foreignKeysNotCoveredWithIndex(final HighAvailabilityPgConnection haPgConnection) {
return new ForeignKeysNotCoveredWithIndexCheckOnCluster(haPgConnection);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,14 @@
public class DbHealthController {

private final HealthLogger healthLogger;
private final PgContext pgContext;

@GetMapping
public ResponseEntity<Collection<String>> collectHealthData() {
final Exclusions exclusions = Exclusions.builder()
.withIndexSizeThreshold(1, MemoryUnit.MB)
.withTableSizeThreshold(1, MemoryUnit.MB)
.build();
return ResponseEntity.ok(healthLogger.logAll(exclusions, PgContext.of("demo")));
return ResponseEntity.ok(healthLogger.logAll(exclusions, pgContext));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright (c) 2019-2024. Ivan Vakhrushev and others.
* https://github.com/mfvanek/pg-index-health-demo
*
* Licensed under the Apache License 2.0
*/

package io.github.mfvanek.pg.index.health.demo.controller;

import io.github.mfvanek.pg.index.health.demo.dto.ForeignKeyMigrationResponse;
import io.github.mfvanek.pg.index.health.demo.dto.MigrationError;
import io.github.mfvanek.pg.index.health.demo.service.DbMigrationGeneratorService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

@RequiredArgsConstructor
@RestController
@RequestMapping("/db/migration")
public class DbMigrationController {

private final DbMigrationGeneratorService dbMigrationGeneratorService;

@PostMapping("/generate")
public ForeignKeyMigrationResponse generateMigrationsWithForeignKeysChecked() {
return dbMigrationGeneratorService.generateMigrationsWithForeignKeysChecked();
}

@ResponseStatus(HttpStatus.EXPECTATION_FAILED)
@org.springframework.web.bind.annotation.ExceptionHandler(IllegalStateException.class)
public MigrationError handleMigrationException(final IllegalStateException illegalStateException) {
return new MigrationError(HttpStatus.EXPECTATION_FAILED.value(), "Migrations failed: " + illegalStateException.getMessage());
mfvanek marked this conversation as resolved.
Show resolved Hide resolved
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright (c) 2019-2024. Ivan Vakhrushev and others.
* https://github.com/mfvanek/pg-index-health-demo
*
* Licensed under the Apache License 2.0
*/

package io.github.mfvanek.pg.index.health.demo.dto;

import io.github.mfvanek.pg.model.constraint.ForeignKey;

import java.util.List;

public record ForeignKeyMigrationResponse(
List<ForeignKey> foreignKeysBefore,
List<ForeignKey> foreignKeysAfter,
List<String> generatedMigrations
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* Copyright (c) 2019-2024. Ivan Vakhrushev and others.
* https://github.com/mfvanek/pg-index-health-demo
*
* Licensed under the Apache License 2.0
*/

package io.github.mfvanek.pg.index.health.demo.dto;

public record MigrationError(
int statusCode,
String message
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Copyright (c) 2019-2024. Ivan Vakhrushev and others.
* https://github.com/mfvanek/pg-index-health-demo
*
* Licensed under the Apache License 2.0
*/

package io.github.mfvanek.pg.index.health.demo.service;

import io.github.mfvanek.pg.common.maintenance.DatabaseCheckOnCluster;
import io.github.mfvanek.pg.generator.DbMigrationGenerator;
import io.github.mfvanek.pg.index.health.demo.dto.ForeignKeyMigrationResponse;
import io.github.mfvanek.pg.model.PgContext;
import io.github.mfvanek.pg.model.constraint.ForeignKey;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.List;
import javax.sql.DataSource;

@Slf4j
@RequiredArgsConstructor
@Service
@Transactional(readOnly = true)
public class DbMigrationGeneratorService {

private final DataSource dataSource;
private final DbMigrationGenerator<ForeignKey> dbMigrationGenerator;
private final DatabaseCheckOnCluster<ForeignKey> foreignKeysNotCoveredWithIndex;
private final PgContext pgContext;

public ForeignKeyMigrationResponse generateMigrationsWithForeignKeysChecked() {
final List<ForeignKey> keysBefore = getForeignKeysFromDb();
final List<String> migrations = generateMigrations(keysBefore);
runGeneratedMigrations(migrations);
final List<ForeignKey> keysAfter = getForeignKeysFromDb();
if (!keysAfter.isEmpty()) {
throw new IllegalStateException("There should be no foreign keys not covered by the index");
}
return new ForeignKeyMigrationResponse(keysBefore, keysAfter, migrations);
}

List<ForeignKey> getForeignKeysFromDb() {
return foreignKeysNotCoveredWithIndex.check(pgContext);
}

private List<String> generateMigrations(final List<ForeignKey> foreignKeys) {
final List<String> generatedMigrations = dbMigrationGenerator.generate(foreignKeys);
log.info("Generated migrations: {}", generatedMigrations);
return generatedMigrations;
}

private void runGeneratedMigrations(final List<String> generatedMigrations) {
try (Connection connection = dataSource.getConnection()) {
for (final String migration : generatedMigrations) {
try (Statement statement = connection.createStatement()) {
statement.execute(migration);
}
}
} catch (SQLException e) {
log.error("Error running migration", e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ class IndexesMaintenanceTest extends BasePgIndexHealthDemoSpringBootTest {
private static final String ORDERS_TABLE = "demo.orders";
private static final String ORDER_ID_COLUMN = "order_id";

private final PgContext demoSchema = PgContext.of("demo");
@Autowired
private PgContext pgContext;

@Autowired
private List<DatabaseCheckOnHost<? extends DbObject>> checks;
Expand Down Expand Up @@ -79,7 +80,7 @@ void databaseStructureCheckForDemoSchema() {
// Skip all runtime checks except SEQUENCE_OVERFLOW
.filter(check -> check.getDiagnostic() == Diagnostic.SEQUENCE_OVERFLOW || check.isStatic())
.forEach(check -> {
final ListAssert<? extends DbObject> checksAssert = assertThat(check.check(demoSchema))
final ListAssert<? extends DbObject> checksAssert = assertThat(check.check(pgContext))
.as(check.getDiagnostic().name());

switch (check.getDiagnostic()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright (c) 2019-2024. Ivan Vakhrushev and others.
* https://github.com/mfvanek/pg-index-health-demo
*
* Licensed under the Apache License 2.0
*/

package io.github.mfvanek.pg.index.health.demo.controller;

import io.github.mfvanek.pg.index.health.demo.dto.MigrationError;
import io.github.mfvanek.pg.index.health.demo.service.DbMigrationGeneratorService;
import io.github.mfvanek.pg.index.health.demo.utils.BasePgIndexHealthDemoSpringBootTest;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;

import static org.assertj.core.api.Assertions.assertThat;

class DbMigrationControllerMockTest extends BasePgIndexHealthDemoSpringBootTest {

@MockBean
DbMigrationGeneratorService dbMigrationGeneratorService;

@Test
void returnsMigrationErrorWhenKeysAfterAreNotEmpty() {
final IllegalStateException illegalStateException = new IllegalStateException("There should be no foreign keys not covered by the index");
Mockito.when(dbMigrationGeneratorService.generateMigrationsWithForeignKeysChecked())
mfvanek marked this conversation as resolved.
Show resolved Hide resolved
.thenThrow(illegalStateException);

final MigrationError result = webTestClient
.post()
.uri(uriBuilder -> uriBuilder
.pathSegment("db", "migration", "generate")
.build())
.accept(MediaType.APPLICATION_JSON)
.headers(this::setUpBasicAuth)
.exchange()
.expectStatus().isEqualTo(HttpStatus.EXPECTATION_FAILED)
.expectBody(MigrationError.class)
.returnResult()
.getResponseBody();

assertThat(result)
.isNotNull()
.isInstanceOf(MigrationError.class);
assertThat(result.message()).contains("Migrations failed: There should be no foreign keys not covered by the index");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Copyright (c) 2019-2024. Ivan Vakhrushev and others.
* https://github.com/mfvanek/pg-index-health-demo
*
* Licensed under the Apache License 2.0
*/

package io.github.mfvanek.pg.index.health.demo.controller;

import io.github.mfvanek.pg.index.health.demo.dto.ForeignKeyMigrationResponse;
import io.github.mfvanek.pg.index.health.demo.dto.MigrationError;
import io.github.mfvanek.pg.index.health.demo.utils.BasePgIndexHealthDemoSpringBootTest;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;

import static org.assertj.core.api.Assertions.assertThat;

class DbMigrationControllerTest extends BasePgIndexHealthDemoSpringBootTest {

@Test
void runsMigrations() {
final ForeignKeyMigrationResponse result = webTestClient
.post()
.uri(uriBuilder -> uriBuilder
.pathSegment("db", "migration", "generate")
.build())
.accept(MediaType.APPLICATION_JSON)
.headers(this::setUpBasicAuth)
.exchange()
.expectStatus().isOk()
.expectBody(ForeignKeyMigrationResponse.class)
.returnResult()
.getResponseBody();

assertThat(result).isNotNull();
assertThat(result.foreignKeysBefore()).isNotEmpty();
assertThat(result.foreignKeysAfter()).isEmpty();
assertThat(result.generatedMigrations()).allMatch(s -> s.contains("create index concurrently if not exists"));
}

@Test
void returnsNothingWithWrongAuthorization() {
final MigrationError result = webTestClient
.post()
.uri(uriBuilder -> uriBuilder
.pathSegment("db", "migration", "generate")
.build())
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus().isEqualTo(HttpStatus.UNAUTHORIZED)
.expectBody(MigrationError.class)
.returnResult()
.getResponseBody();

assertThat(result).isNull();
}
}
Loading