Skip to content

Commit

Permalink
Add model for duplicated and intersected foreign keys (#450)
Browse files Browse the repository at this point in the history
* Update Gradle to 8.10.2

* Update PostgreSQL versions

* Update sql queries

* Update dependencies

* Add DuplicatedForeignKeys class

* Add tests for Validators

* Add tests for DuplicatedForeignKeys
  • Loading branch information
mfvanek authored Oct 6, 2024
1 parent a544d40 commit 76da7f7
Show file tree
Hide file tree
Showing 27 changed files with 408 additions and 66 deletions.
8 changes: 4 additions & 4 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
strategy:
fail-fast: false
matrix:
pg_version: [ "12.18", "13.14", "14.11", "15.6", "16.2" ]
pg_version: [ "12.20", "13.16", "14.13", "15.8", "16.4" ]
env:
TEST_PG_VERSION: ${{ matrix.pg_version }}
runs-on: ubuntu-latest
Expand Down Expand Up @@ -43,17 +43,17 @@ jobs:
restore-keys: |
${{ runner.os }}-gradle-
- name: Build with Gradle and analyze
if: matrix.pg_version == '14.11'
if: matrix.pg_version == '14.13'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_DASHBOARD_API_KEY }}
run: ./gradlew build sonarqube --info
- name: Build with Gradle
if: matrix.pg_version != '14.11'
if: matrix.pg_version != '14.13'
run: ./gradlew build
- name: Upload coverage to Codecov
if: matrix.pg_version == '14.11'
if: matrix.pg_version == '14.13'
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Java >= 11 is required.

This will build the project and run tests.

By default, [PostgreSQL 16.2 from Testcontainers](https://www.testcontainers.org/) is used to run tests.
By default, [PostgreSQL 16.4 from Testcontainers](https://www.testcontainers.org/) is used to run tests.
Set `TEST_PG_VERSION` environment variable to use any of other available PostgreSQL version:
```
TEST_PG_VERSION=11.20-alpine
Expand Down
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ dependencies {

tasks{
wrapper {
gradleVersion = "8.7"
gradleVersion = "8.10.2"
}

check {
Expand Down
8 changes: 4 additions & 4 deletions buildSrc/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ repositories {
}

dependencies {
implementation("com.github.spotbugs.snom:spotbugs-gradle-plugin:6.0.9")
implementation("org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:5.0.0.4638")
implementation("net.ltgt.gradle:gradle-errorprone-plugin:3.1.0")
implementation("com.github.spotbugs.snom:spotbugs-gradle-plugin:6.0.24")
implementation("org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:5.1.0.4882")
implementation("net.ltgt.gradle:gradle-errorprone-plugin:4.0.1")
implementation("info.solidsoft.gradle.pitest:gradle-pitest-plugin:1.15.0")
implementation("org.gradle:test-retry-gradle-plugin:1.5.9")
implementation("org.gradle:test-retry-gradle-plugin:1.6.0")
implementation(libs.forbiddenapis)
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ plugins {
}

dependencies {
errorprone("com.google.errorprone:error_prone_core:2.27.1")
errorprone("jp.skypencil.errorprone.slf4j:errorprone-slf4j:0.1.24")
errorprone("com.google.errorprone:error_prone_core:2.33.0")
errorprone("jp.skypencil.errorprone.slf4j:errorprone-slf4j:0.1.28")

spotbugsPlugins("jp.skypencil.findbugs.slf4j:bug-pattern:1.5.0")
spotbugsPlugins("com.h3xstream.findsecbugs:findsecbugs-plugin:1.13.0")
spotbugsPlugins("com.mebigfatguy.sb-contrib:sb-contrib:7.6.4")
spotbugsPlugins("com.mebigfatguy.sb-contrib:sb-contrib:7.6.5")
}

tasks.withType<JavaCompile>().configureEach {
Expand All @@ -39,7 +39,7 @@ tasks {

withType<Test>().configureEach {
retry {
maxRetries.set(3)
maxRetries.set(2)
maxFailures.set(10)
failOnPassedAfterRetry.set(false)
}
Expand Down
2 changes: 1 addition & 1 deletion buildSrc/src/main/kotlin/pg-index-health.pitest.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ dependencies {

pitest {
junit5PluginVersion.set("1.2.1")
pitestVersion.set("1.16.1")
pitestVersion.set("1.17.0")
threads.set(4)
if (System.getenv("STRYKER_DASHBOARD_API_KEY") != null) {
outputFormats.set(setOf("stryker-dashboard"))
Expand Down
2 changes: 1 addition & 1 deletion docker/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ version: "3.9"

services:
postgres:
image: postgres:16.2
image: postgres:16.4
shm_size: "2gb"
environment:
POSTGRES_DB: "pgih-db"
Expand Down
10 changes: 5 additions & 5 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ spring-boot = "2.7.18"
detekt = "1.23.6"
commons-lang3 = "3.17.0"
slf4j = "1.7.36" # to be compatible with Spring Boot 2.7.X
assertj = "3.25.3"
testcontainers = "1.20.1"
junit = "5.10.2"
mockito = "5.12.0"
assertj = "3.26.3"
testcontainers = "1.20.2"
junit = "5.11.2"
mockito = "5.14.1"
forbiddenapis = "3.7"

[libraries]
Expand All @@ -18,7 +18,7 @@ slf4j-simple = { group = "org.slf4j", name = "slf4j-simple", version.ref = "slf4
apache-commons-dbcp2 = "org.apache.commons:commons-dbcp2:2.12.0"
awaitility = "org.awaitility:awaitility:4.2.2"
apache-commons-lang3 = { group = "org.apache.commons", name = "commons-lang3", version.ref = "commons-lang3" }
equalsverifier = "nl.jqno.equalsverifier:equalsverifier:3.17"
equalsverifier = "nl.jqno.equalsverifier:equalsverifier:3.17.1"
spring-boot-starter-root = { group = "org.springframework.boot", name = "spring-boot-starter", version.ref = "spring-boot" }
spring-boot-starter-test = { group = "org.springframework.boot", name = "spring-boot-starter-test", version.ref = "spring-boot" }
spring-boot-autoconfigure-processor = { group = "org.springframework.boot", name = "spring-boot-autoconfigure-processor", version.ref = "spring-boot" }
Expand Down
Binary file modified gradle/wrapper/gradle-wrapper.jar
Binary file not shown.
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
Expand Down
7 changes: 5 additions & 2 deletions gradlew
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#

##############################################################################
#
Expand Down Expand Up @@ -55,7 +57,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
Expand Down Expand Up @@ -84,7 +86,8 @@ done
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$PWD" ) || exit

# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
Expand Down
2 changes: 2 additions & 0 deletions gradlew.bat
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem

@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ final class PostgresVersionTest extends DatabaseAwareTestBase {
@Test
void checkPgVersion() {
final String pgVersionFromEnv = System.getenv(PG_VERSION_ENVIRONMENT_VARIABLE);
final String requiredPgVersionString = (pgVersionFromEnv == null) ? "16.2 (Debian 16.2-" : pgVersionFromEnv.split("-")[0];
final String requiredPgVersionString = (pgVersionFromEnv == null) ? "16.4 (Debian 16.4-" : pgVersionFromEnv.split("-")[0];
final String actualPgVersionString = PostgresVersionReader.readVersion(getDataSource());
assertThat(actualPgVersionString)
.startsWith(requiredPgVersionString);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/*
* Copyright (c) 2019-2024. Ivan Vakhrushev and others.
* https://github.com/mfvanek/pg-index-health
*
* This file is a part of "pg-index-health" - a Java library for
* analyzing and maintaining indexes health in PostgreSQL databases.
*
* Licensed under the Apache License 2.0
*/

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

import io.github.mfvanek.pg.model.DbObject;
import io.github.mfvanek.pg.model.index.utils.DuplicatedIndexesParser;
import io.github.mfvanek.pg.model.table.TableNameAware;
import io.github.mfvanek.pg.model.validation.Validators;

import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import javax.annotation.concurrent.Immutable;

/**
* A representation of duplicated foreign keys in a database.
*
* @author Ivan Vakhrushev
* @see TableNameAware
*/
@Immutable
public class DuplicatedForeignKeys implements DbObject, TableNameAware {

private final List<ForeignKey> foreignKeys;
private final List<String> foreignKeysNames;

private DuplicatedForeignKeys(@Nonnull final List<ForeignKey> foreignKeys) {
final List<ForeignKey> defensiveCopy = List.copyOf(Objects.requireNonNull(foreignKeys, "foreignKeys cannot be null"));
Validators.validateThatTableIsTheSame(defensiveCopy);
this.foreignKeys = defensiveCopy;
this.foreignKeysNames = this.foreignKeys.stream()
.map(ForeignKey::getConstraintName)
.collect(Collectors.toUnmodifiableList());
}

/**
* {@inheritDoc}
*/
@Nonnull
@Override
public final String getName() {
return String.join(",", foreignKeysNames);
}

/**
* {@inheritDoc}
*/
@Override
@Nonnull
public String getTableName() {
return foreignKeys.get(0).getTableName();
}

/**
* Gets duplicated foreign keys.
*
* @return list of duplicated foreign keys
* @see ForeignKey
*/
@Nonnull
public List<ForeignKey> getForeignKeys() {
return foreignKeys;
}

/**
* {@inheritDoc}
*/
@Override
public final boolean equals(final Object other) {
if (this == other) {
return true;
}

if (!(other instanceof DuplicatedForeignKeys)) {
return false;
}

final DuplicatedForeignKeys that = (DuplicatedForeignKeys) other;
return Objects.equals(foreignKeys, that.foreignKeys);
}

/**
* {@inheritDoc}
*/
@Override
public final int hashCode() {
return Objects.hash(foreignKeys);
}

/**
* {@inheritDoc}
*/
@Nonnull
@Override
public String toString() {
return DuplicatedForeignKeys.class.getSimpleName() + '{' +
"tableName='" + getTableName() + '\'' +
", foreignKeys=" + foreignKeys +
'}';
}

/**
* Constructs an {@code DuplicatedForeignKeys} object from given list of foreign keys.
*
* @param foreignKeys list of duplicated foreign keys; should be non-null.
* @return {@code DuplicatedForeignKeys}
*/
@Nonnull
public static DuplicatedForeignKeys of(@Nonnull final List<ForeignKey> foreignKeys) {
return new DuplicatedForeignKeys(foreignKeys);
}

/**
* Constructs an {@code DuplicatedForeignKeys} object from given foreign keys.
*
* @param firstForeignKey first foreign key; should be non-null.
* @param secondForeignKey second foreign key; should be non-null.
* @param otherForeignKeys other foreign keys.
* @return {@code DuplicatedForeignKeys}
*/
@Nonnull
public static DuplicatedForeignKeys of(@Nonnull final ForeignKey firstForeignKey,
@Nonnull final ForeignKey secondForeignKey,
@Nonnull final ForeignKey... otherForeignKeys) {
return new DuplicatedForeignKeys(DuplicatedIndexesParser.combine(firstForeignKey, secondForeignKey, otherForeignKeys));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.annotation.Nonnull;
import javax.annotation.concurrent.Immutable;

Expand All @@ -44,7 +43,7 @@ public class DuplicatedIndexes implements DbObject, TableNameAware {

private DuplicatedIndexes(@Nonnull final List<IndexWithSize> duplicatedIndexes) {
final List<IndexWithSize> defensiveCopy = List.copyOf(Objects.requireNonNull(duplicatedIndexes, "duplicatedIndexes cannot be null"));
validateThatTableIsTheSame(defensiveCopy);
Validators.validateThatTableIsTheSame(defensiveCopy);
this.indexes = defensiveCopy.stream()
.sorted(INDEX_WITH_SIZE_COMPARATOR)
.collect(Collectors.toUnmodifiableList());
Expand Down Expand Up @@ -174,37 +173,13 @@ public static DuplicatedIndexes of(@Nonnull final String tableName, @Nonnull fin
*
* @param firstIndex first index; should be non-null.
* @param secondIndex second index; should be non-null.
* @param otherIndexes other indexes
* @param otherIndexes other indexes.
* @return {@code DuplicatedIndexes}
*/
@Nonnull
public static DuplicatedIndexes of(@Nonnull final IndexWithSize firstIndex,
@Nonnull final IndexWithSize secondIndex,
@Nonnull final IndexWithSize... otherIndexes) {
Objects.requireNonNull(firstIndex, "firstIndex cannot be null");
Objects.requireNonNull(secondIndex, "secondIndex cannot be null");
if (Stream.of(otherIndexes).anyMatch(Objects::isNull)) {
throw new IllegalArgumentException("otherIndexes cannot contain nulls");
}
final Stream<IndexWithSize> basePart = Stream.of(firstIndex, secondIndex);
return new DuplicatedIndexes(Stream.concat(basePart, Stream.of(otherIndexes))
.collect(Collectors.toUnmodifiableList()));
}

private static void validateThatTableIsTheSame(@Nonnull final List<? extends TableNameAware> duplicatedIndexes) {
final String tableName = validateThatContainsAtLeastTwoRows(duplicatedIndexes).get(0).getTableName();
Validators.validateThatTableIsTheSame(tableName, duplicatedIndexes);
}

@Nonnull
private static <T> List<T> validateThatContainsAtLeastTwoRows(@Nonnull final List<T> duplicatedIndexes) {
final int size = Objects.requireNonNull(duplicatedIndexes).size();
if (0 == size) {
throw new IllegalArgumentException("duplicatedIndexes cannot be empty");
}
if (size < 2) {
throw new IllegalArgumentException("duplicatedIndexes should contains at least two rows");
}
return duplicatedIndexes;
return new DuplicatedIndexes(DuplicatedIndexesParser.combine(firstIndex, secondIndex, otherIndexes));
}
}
Loading

0 comments on commit 76da7f7

Please sign in to comment.