Skip to content

AOT-generated repository fails to compile for query methods returning Set #5225

Description

@maryantocinn

Summary

A derived MongoRepository query returning Set<T> works in regular JVM mode but fails during AOT compilation.

The generated implementation returns ExecutableFindOperation.TerminatingResults#all(), which produces List<T>, without adapting it to the declared Set<T> return type.

Minimal reproduction project

https://github.com/maryantocinn/spring-data-mongodb-aot-set-reproducer

Environment

  • Spring Boot: 4.1.0
  • Spring Data MongoDB: 5.1.0
  • Java: 25
  • Gradle: 9.5.1
  • GraalVM Build Tools: 1.1.1

Steps to reproduce

git clone https://github.com/maryantocinn/spring-data-mongodb-aot-set-reproducer.git
cd spring-data-mongodb-aot-set-reproducer
./gradlew clean compileAotJava

A MongoDB instance is not required because the failure occurs while compiling the generated AOT sources.

The repository declares:

public interface PersonRepository extends MongoRepository<Person, String> {

  Set<Person> findAllByName(String name);
}

Expected behavior

The AOT-generated repository should adapt the query result to Set<Person> and compile successfully, matching regular non-AOT repository behavior.

Actual behavior

AOT generates:

public Set<Person> findAllByName(String name) {
  // ...
  return finder.matching(filterQuery).all();
}

Compilation fails:

PersonRepositoryImpl__AotRepository.java:34: error:
incompatible types: List<Person> cannot be converted to Set<Person>
    return finder.matching(filterQuery).all();
                                           ^

Workaround

Change the repository return type to List<T> and convert it to a Set in application code when required.

Additional context

The AOT generator selects all() for collection queries:

String terminatingMethod;
if (queryMethod.isCollectionQuery() || queryMethod.isPageQuery() || queryMethod.isSliceQuery()) {
terminatingMethod = "all()";
} else if (query.isCount()) {
terminatingMethod = "count()";
} else if (query.isExists()) {
terminatingMethod = "exists()";
} else if (queryMethod.isStreamQuery()) {
terminatingMethod = "stream()";
} else {
if (query.getQuery().isLimited()) {
terminatingMethod = Optional.class.isAssignableFrom(methodReturn.toClass()) ? "first()"
: "firstValue()";
} else {
terminatingMethod = Optional.class.isAssignableFrom(methodReturn.toClass()) ? "one()"
: "oneValue()";
}
}
if (queryMethod.isPageQuery()) {
builder.addStatement("return new $T($L, $L).execute($L)", PagedExecution.class, context.localVariable("finder"),
context.getPageableParameterName(), query.name());
} else if (queryMethod.isSliceQuery()) {
builder.addStatement("return new $T($L, $L).execute($L)", SlicedExecution.class,
context.localVariable("finder"), context.getPageableParameterName(), query.name());
} else if (queryMethod.isScrollQuery()) {
String scrollPositionParameterName = context.getScrollPositionParameterName();
if (scrollPositionParameterName != null) {
builder.addStatement("return $L.matching($L).scroll($L)", context.localVariable("finder"), query.name(),
scrollPositionParameterName);
} else {
String pageableParameterName = context.getPageableParameterName();
if (pageableParameterName != null) {
builder.addStatement("return $L.matching($L).scroll($L.toScrollPosition())",
context.localVariable("finder"), query.name(), pageableParameterName);
} else {
builder.addStatement("return $L.matching($L).scroll($T.initial())", context.localVariable("finder"),
query.name(), ScrollPosition.class);
}
}
} else {
if (query.isCount() && !ClassUtils.isAssignable(Long.class, methodReturn.getActualReturnClass())) {
Class<?> returnType = ClassUtils.resolvePrimitiveIfNecessary(queryMethod.getReturnedObjectType());
builder.addStatement("return $T.convertNumberToTargetClass($L.matching($L).$L, $T.class)", NumberUtils.class,
context.localVariable("finder"), query.name(), terminatingMethod, returnType);
} else {
CodeBlock resultBlock = CodeBlock.of("$L.matching($L).$L", context.localVariable("finder"), query.name(),
terminatingMethod);
builder.addStatement("return $L", MongoCodeBlocks.potentiallyWrapStreamable(methodReturn, resultBlock));

all() returns List<T>:

/**
* Get all matching elements.
*
* @return never {@literal null}.
*/
List<T> all();

The result adaptation handles Streamable, but not Set:

/**
* Wraps the given {@link CodeBlock} representing an {@link Iterable} into a {@link Streamable} if the
* {@link MethodReturn} indicates so.
*/
public static CodeBlock potentiallyWrapStreamable(MethodReturn methodReturn, CodeBlock returningIterable) {
Class<?> returnType = methodReturn.toClass();
if (returnType.equals(Streamable.class)) {
return CodeBlock.of("$T.of($L)", Streamable.class, returningIterable);
}
if (ClassUtils.isAssignable(Streamable.class, returnType)) {
return CodeBlock.of(
"($1T) $2T.getSharedInstance().convert($3T.of($4L), $5T.valueOf($3T.class), $5T.valueOf($1T.class))",
returnType, DefaultConversionService.class, Streamable.class, returningIterable, TypeDescriptor.class);
}
return returningIterable;

A similar issue was fixed for Spring Data JPA:

spring-projects/spring-data-jpa#4094

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions