#4278·flyway

Performance: validateOnMigrate slows exponentially as the number of migration scripts increases

Author: joelsearcyCreated Sep 2, 2026Updated Sep 18, 2026
Labelss: needs investigation

Background

I'm working on improve CI build times on a database project that has over 4400 migration scripts across dozens of sub-directories, and with validateOnMigrate = True the validation step accounts for over 50 seconds of the runtime.

I thought that seemed odd, given the operations I would expect it to be performing, so I cloned the source code and started profiling it.

Looking through the code and the profiling, it looks like there are a number of places that are using streaming concepts (which is quite readable) that are unfortunately resulting in rescanning the enumeration stream in multiple places per migration script. In particular, it looks like the call to validation migration scripts refreshes the configuration, and then the calling code immediate refreshes the configuration metadata again.

So, pulling on the thread of hotspots in the profiling, it looks like highestSHTVersion & hasFutureUndo via calculateNoSHTStates are significant contributors to this exponential slowness:

By CPU % over 2880 migration scripts:

highestSHTVersion              93.1%
hasFutureUndo                  92.6%
calculateNoSHTStates           93.4%
insertResolvedMigrations        1.9%
scanForResources                1.0%
ResourceNameParser              0.6%
PluginRegister                  0.5%
ChecksumCalculator              0.3%

Example of the current pattern from CoreMigrationStateCalculator.java:

java
    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()));
    }

I propose refactoring the repeated stream -> filter patterns in this class to essentially process once and cache the results for re-use in the same execution. From what I can tell, this is only needed once per execution, and if fast shouldn't need to be recalculated by later steps.

I plan to work on implementing the necessary changes along with a relevant benchmark, as I keep pulling on the profiler thread in the code paths for validating the migration scripts.