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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions