Skip to content

NativeImageResourceProvider does not find Flyway migration scripts in subdirectories #49661

@sondemar

Description

@sondemar

Follows from #31999, which introduced NativeImageResourceProvider as the solution for Flyway classpath scanning in native images. This reports a gap in that implementation: subdirectory migration locations are not supported.

Problem

NativeImageResourceProvider uses a single-level glob when scanning the classpath migration locations at runtime in a native image:

// NativeImageResourceProvider.java
private Resource[] getResources(PathMatchingResourcePatternResolver resolver, Location location, Resource root) {
    try {
        return resolver.getResources(root.getURI() + "/*");
    }
    ...
}

The /* pattern matches only files directly under the root location. Migration script placed in subdirectories (a common convention in larger projects, e.g. db/migration/1.0.0/V1__init.sql, db/migration/2.0.0/V2__add_column.sql) are silently skipped, causing Flyway to find no migrations and fail or produce an empty schema.

Note: the resources are correctly bundled in the native image when resource hints register db/migration/**. The failure is purely in runtime discovery.

Expected behaviour

All migration scripts under a configured location are found regardless of nesting depth, consistent with the behaviour of Flyway on a regular JVM.

Actual behaviour

Only scripts directly under the location root (e.g. db/migration/V1__init.sql) are found. Scripts in subdirectories (e.g. db/migration/1.0.0/V1__init.sql) are not discovered and Flyway does not run them.

Proposed fix

Change /* to /**/* in getResources():

  // before
  return resolver.getResources(root.getURI() + "/*");

  // after
  return resolver.getResources(root.getURI() + "/**/*");

This also requires fixing asClassPathResource to keep the full subdirectory path. With /*, resources are always one level deep, so location.getRootPath() + "/" + filename is correct. When changed to /**/*, subdirectory resources appear and that construction flattens db/migration/1.0.0/V1__init.sql incorrectly to db/migration/V1__init.sql:

  // before — correct only for flat locations, breaks with /**/*
  String fileNameWithAbsolutePath = location.getRootPath() + "/" + locatedResource.resource().getFilename();

  // after — uses the full classpath-relative path from the resolver result
  private String resolveClasspathPath(Resource resource, Location location) {
      if (resource instanceof ClassPathResource cpr) {
          return cpr.getPath(); // e.g. "db/migration/1.0.0/V1__init.sql"
      }
      return location.getRootPath() + "/" + resource.getFilename(); // fallback
  }

Reproduce

  PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
  Resource root = resolver.getResource("classpath:db/migration");

  // current — misses subdirectory scripts
  Resource[] shallow = resolver.getResources(root.getURI() + "/*");

  // expected
  Resource[] recursive = resolver.getResources(root.getURI() + "/**/*");

With db/migration/V1__flat.sql and db/migration/1.0.0/V2__subdir.sql on the classpath, shallow contains only V1__flat.sql but recursive contains both.

Spring Boot version: 4.0.0

Metadata

Metadata

Assignees

Labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions