CoreMigrationStateCalculator.java
/*-
* ========================LICENSE_START=================================
* flyway-nc-core
* ========================================================================
* Copyright (C) 2010 - 2025 Red Gate Software Ltd
* ========================================================================
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* =========================LICENSE_END==================================
*/
package org.flywaydb.nc.info;
import java.util.Collection;
import java.util.Comparator;
import java.util.Optional;
import org.flywaydb.core.api.CoreMigrationType;
import org.flywaydb.core.api.MigrationState;
import org.flywaydb.core.api.MigrationVersion;
import org.flywaydb.core.api.configuration.Configuration;
import org.flywaydb.core.api.resource.LoadableResourceMetadata;
import org.flywaydb.core.internal.nc.NativeConnectorsStateCalculator;
import org.flywaydb.core.internal.nc.schemahistory.ResolvedSchemaHistoryItem;
import org.flywaydb.core.internal.util.Pair;
public class CoreMigrationStateCalculator implements NativeConnectorsStateCalculator {
public MigrationState calculateState(final Pair<ResolvedSchemaHistoryItem, LoadableResourceMetadata> migration,
final Collection<? extends Pair<ResolvedSchemaHistoryItem, LoadableResourceMetadata>> sortedMigrations,
final Configuration configuration) {
if (migration.getLeft() == null) {
return calculateNoSHTStates(migration, sortedMigrations, configuration);
}
return calculateSHTStates(migration, sortedMigrations);
}
private static MigrationState calculateNoSHTStates(final Pair<ResolvedSchemaHistoryItem, LoadableResourceMetadata> migration,
final Collection<? extends Pair<ResolvedSchemaHistoryItem, LoadableResourceMetadata>> sortedMigrations,
final Configuration configuration) {
Optional<MigrationVersion> baselineVersion = sortedMigrations.stream()
.filter(x -> x.getLeft() != null)
.filter(x -> x.getLeft().getType().isBaseline())
.map(x -> x.getLeft().getVersion())
.findFirst();
final boolean baselinedSchema = baselineVersion.isPresent();
if (baselineVersion.isEmpty()) {
baselineVersion = sortedMigrations.stream()
.filter(x -> x.getRight() != null)
.filter(x -> x.getRight().migrationType().isBaseline())
.map(x -> x.getRight().version())
.max(MigrationVersion::compareTo);
}
if (baselineVersion.isEmpty()
|| migration.getRight().isRepeatable()
|| migration.getRight().version().isNewerThan(baselineVersion.get())) {
final MigrationVersion target = configuration.getTarget();
if (migration.getRight().isRepeatable()) {
return MigrationState.PENDING;
}
if (target != null && migration.getRight().version().isNewerThan(target)) {
return MigrationState.ABOVE_TARGET;
}
if (migration.getRight().migrationType().isUndo()) {
return MigrationState.AVAILABLE;
}
if (migration.getRight().sqlScriptMetadata() != null && !migration.getRight()
.sqlScriptMetadata()
.shouldExecute()) {
return MigrationState.IGNORED;
}
if (migration.getRight().migrationType().isBaseline() && baselinedSchema) {
return MigrationState.IGNORED;
}
if (!configuration.isOutOfOrder()) {
final MigrationVersion highestSHTVersion = highestSHTVersion(sortedMigrations);
if (migration.getRight().version().isNewerThan(highestSHTVersion)) {
return MigrationState.PENDING;
}
return MigrationState.IGNORED;
}
return MigrationState.PENDING;
} else if (migration.getRight().version().equals(baselineVersion.get())) {
return migration.getRight().migrationType().isBaseline() && !baselinedSchema
? MigrationState.PENDING
: MigrationState.BASELINE_IGNORED;
} else {
return MigrationState.BELOW_BASELINE;
}
}
private static MigrationState calculateSHTStates(final Pair<ResolvedSchemaHistoryItem, LoadableResourceMetadata> migration,
final Collection<? extends Pair<ResolvedSchemaHistoryItem, LoadableResourceMetadata>> sortedMigrations) {
if (migration.getLeft().getType() == CoreMigrationType.SCHEMA) {
return MigrationState.SUCCESS;
}
if (migration.getLeft().getType().isBaseline()) {
return migration.getLeft().isSuccess() ? MigrationState.BASELINE : MigrationState.FAILED;
}
if (migration.getLeft().isSuccess()) {
final MigrationState lookAheadState = calculateLookAheadStates(migration, sortedMigrations);
if (lookAheadState != null) {
return lookAheadState;
}
if (migration.getRight() != null) {
return MigrationState.SUCCESS;
}
if (migration.getLeft().isVersioned()) {
final MigrationState missingState = calculateMissingStates(migration, sortedMigrations);
if (missingState != null) {
return missingState;
}
}
if (migration.getLeft().isRepeatable() && migration.getLeft().isSuccess()) {
final MigrationState repeatableState = calculateRepeatableStates(migration, sortedMigrations);
if (repeatableState != null) {
return repeatableState;
}
}
return MigrationState.SUCCESS;
}
if (migration.getRight() == null) {
final MigrationVersion maxLocalVersion = highestLocalVersion(sortedMigrations);
if (migration.getLeft().isRepeatable()) {
return MigrationState.MISSING_FAILED;
}
return migration.getLeft().getVersion().isNewerThan(maxLocalVersion)
? MigrationState.FUTURE_FAILED
: MigrationState.MISSING_FAILED;
}
return MigrationState.FAILED;
}
private static MigrationVersion highestLocalVersion(final Collection<? extends Pair<ResolvedSchemaHistoryItem, LoadableResourceMetadata>> sortedMigrations) {
return sortedMigrations.stream()
.filter(x -> x.getRight() != null)
.map(Pair::getRight)
.filter(LoadableResourceMetadata::isVersioned)
.filter(x -> !x.migrationType().isUndo())
.map(LoadableResourceMetadata::version)
.max(Comparator.naturalOrder())
.orElse(MigrationVersion.EMPTY);
}
private static MigrationVersion highestSHTVersion(final Collection<? extends Pair<ResolvedSchemaHistoryItem, LoadableResourceMetadata>> sortedMigrations) {
return sortedMigrations.stream()
.filter(x -> x.getLeft() != null)
.filter(x -> !hasFutureUndo(x, sortedMigrations))
.map(Pair::getLeft)
.filter(ResolvedSchemaHistoryItem::isVersioned)
.filter(x -> !x.getType().isUndo())
.map(ResolvedSchemaHistoryItem::getVersion)
.max(Comparator.naturalOrder())
.orElse(MigrationVersion.EMPTY);
}
private static MigrationState calculateLookAheadStates(final Pair<ResolvedSchemaHistoryItem, LoadableResourceMetadata> migration,
final Collection<? extends Pair<ResolvedSchemaHistoryItem, LoadableResourceMetadata>> sortedMigrations) {
if (!migration.getLeft().getType().isUndo() && hasFutureUndo(migration, sortedMigrations)) {
return MigrationState.UNDONE;
}
final boolean futureDelete = sortedMigrations.stream()
.filter(x -> x.getLeft() != null)
.filter(x -> x.getLeft().getType() == CoreMigrationType.DELETE)
.filter(x -> x.getLeft().isRepeatable() == migration.getLeft().isRepeatable())
.anyMatch(x -> x.getLeft().isRepeatable()
? x.getLeft()
.getDescription()
.equals(migration.getLeft().getDescription())
: x.getLeft().getVersion().equals(migration.getLeft().getVersion()));
if (futureDelete && migration.getLeft().getType() != CoreMigrationType.DELETE) {
return MigrationState.DELETED;
}
if (migration.getLeft().isVersioned() && !migration.getLeft().getType().isUndo()) {
final boolean outOfOrder = sortedMigrations.stream()
.filter(x -> x.getLeft() != null)
.filter(x -> !x.getLeft().getType().isUndo())
.filter(x -> x.getLeft().isVersioned())
.filter(x -> x.getLeft().getVersion().isNewerThan(migration.getLeft().getVersion()))
.anyMatch(x -> x.getLeft().getInstalledRank() < migration.getLeft().getInstalledRank());
if (outOfOrder) {
return MigrationState.OUT_OF_ORDER;
}
}
return null;
}
private static boolean hasFutureUndo(final Pair<ResolvedSchemaHistoryItem, LoadableResourceMetadata> migration,
final Collection<? extends Pair<ResolvedSchemaHistoryItem, LoadableResourceMetadata>> sortedMigrations) {
return sortedMigrations.stream()
.filter(x -> x.getLeft() != null)
.filter(x -> x.getLeft().getType().isUndo())
.filter(x -> x.getLeft().getInstalledRank() > migration.getLeft().getInstalledRank())
.anyMatch(x -> x.getLeft().getVersion().equals(migration.getLeft().getVersion()));
}
private static MigrationState calculateMissingStates(final Pair<ResolvedSchemaHistoryItem, LoadableResourceMetadata> migration,
final Collection<? extends Pair<ResolvedSchemaHistoryItem, LoadableResourceMetadata>> sortedMigrations) {
final MigrationVersion latestLocalVersion = sortedMigrations.stream()
.filter(x -> x.getRight() != null)
.filter(x -> x.getRight().isVersioned())
.map(x -> x.getRight().version())
.sorted()
.findFirst()
.orElse(MigrationVersion.EMPTY);
if (migration.getLeft().getVersion().isNewerThan(latestLocalVersion)) {
return MigrationState.FUTURE_SUCCESS;
}
if (latestLocalVersion.isNewerThan(migration.getLeft().getVersion())) {
return MigrationState.MISSING_SUCCESS;
}
return null;
}
private static MigrationState calculateRepeatableStates(final Pair<ResolvedSchemaHistoryItem, LoadableResourceMetadata> migration,
final Collection<? extends Pair<ResolvedSchemaHistoryItem, LoadableResourceMetadata>> sortedMigrations) {
final boolean superseded = sortedMigrations.stream()
.filter(x -> x.getLeft() != null)
.filter(x -> x.getLeft().isSuccess())
.filter(x -> x.getLeft().isRepeatable())
.filter(x -> x.getLeft().getDescription().equals(migration.getLeft().getDescription()))
.anyMatch(x -> x.getLeft().getInstalledRank() > migration.getLeft().getInstalledRank());
if (superseded) {
return MigrationState.SUPERSEDED;
}
final boolean outdated = sortedMigrations.stream()
.filter(x -> x.getLeft() == null)
.filter(x -> x.getRight().isRepeatable())
.anyMatch(x -> x.getRight().description().equals(migration.getLeft().getDescription()));
if (outdated) {
return MigrationState.OUTDATED;
}
if (migration.getRight() == null) {
return MigrationState.MISSING_SUCCESS;
}
return null;
}
}