Skip to content

Conversation

mp911de
Copy link
Member

@mp911de mp911de commented Aug 29, 2025

We now provide AOT support to generate repository implementations during build-time for Cassandra repository query methods.

Supported Features

  • Derived query methods, @Query and named query methods.
  • Window, Slice, Stream, and Optional return types
  • Sort query rewriting
  • Interface and DTO Projections
  • Value Expressions (Those require a bit of reflective information.
    Mind that using Value Expressions requires expression parsing and contextual information to evaluate the expression)

Limitation

  • Vector Search not yet supported

Excluded methods

  • CrudRepository and other base interface methods as their implementation is provided by the base class respective fragments
  • Vector Search Methods

Example repository:

interface PersonRepository extends CrudRepository<Person, String> {

	@Query(idempotent = Query.Idempotency.IDEMPOTENT)
	Person findByFirstname(String firstname);

	Stream<PersonDto> streamDtoProjectionByFirstname(String firstname);

	@Query("select numberOfChildren from person where firstname = :firstname")
	int findDeclaredNumberOfChildrenByFirstname(String firstname);

	Window<Person> findWindowByLastname(String lastname, ScrollPosition scrollPosition, Limit limit);
}

Generated fragment:

/**
 * AOT generated Cassandra repository implementation for {@link PersonRepository}.
 */
public class PersonRepositoryImpl__AotRepository extends AotRepositoryFragmentSupport {
  private final CassandraOperations operations;

  public PersonRepositoryImpl__AotRepository(CassandraOperations operations,
      RepositoryFactoryBeanSupport.FragmentCreationContext context) {
    super(operations, context);
    this.operations = operations;
  }

  /**
   * AOT generated implementation of {@link PersonRepository#findByFirstname(java.lang.String)}.
   */
  public Person findByFirstname(String firstname) {
    Query query = Query.query(Criteria.where("firstname").is(firstname));

    ExecutableSelectOperation.TerminatingSelect<Person> select = operations.query(Person.class).matching(query);
    return select.oneValue();
  }

  /**
   * AOT generated implementation of {@link PersonRepository#streamDtoProjectionByFirstname(java.lang.String)}.
   */
  public Stream<PersonRepository.PersonDto> streamDtoProjectionByFirstname(String firstname) {
    Query query = Query.query(Criteria.where("firstname").is(firstname));

    ExecutableSelectOperation.TerminatingSelect<PersonRepository.PersonDto> select = operations.query(Person.class).as(PersonRepository.PersonDto.class).matching(query);
    return select.stream();
  }

  /**
   * AOT generated implementation of {@link PersonRepository#findDeclaredNumberOfChildrenByFirstname(java.lang.String)}.
   */
  public int findDeclaredNumberOfChildrenByFirstname(String firstname) {
    Object[] args = new Object[1];
    args[0] = potentiallyConvertBindingValue(firstname);
    SimpleStatement query = SimpleStatement.newInstance("select numberOfChildren from person where firstname = ?", args);

    ExecutableSelectOperation.TerminatingResults<Integer> select = operations.query(query).as(Integer.class);
    return select.oneValue();
  }

  /**
   * AOT generated implementation of {@link PersonRepository#findWindowByLastname(java.lang.String, org.springframework.data.domain.ScrollPosition, org.springframework.data.domain.Limit)}.
   */
  public Window<Person> findWindowByLastname(String lastname, ScrollPosition scrollPosition,
      Limit limit) {
    Query query = Query.query(Criteria.where("lastname").is(lastname));
    if (limit.isLimited()) {
      query = query.limit(limit.max() + 1);
    }
    if (!scrollPosition.isInitial()) {
      query = query.pagingState((CassandraScrollPosition) scrollPosition);
    }
    QueryOptions.QueryOptionsBuilder optionsBuilder = QueryOptions.builder();
    if (limit.isLimited()) {
      optionsBuilder.pageSize(limit.max());
    }
    query = query.queryOptions(optionsBuilder.build());

    ExecutableSelectOperation.TerminatingSelect<Person> select = operations.query(Person.class).matching(query);
    return WindowUtil.of(select.slice());
  }
  
}

Metadata (truncated):

{
  "name": "org.springframework.data.cassandra.repository.aot.PersonRepository",
  "module": "Cassandra",
  "type": "IMPERATIVE",
  "methods": [
    {
      "name": "findByFirstname",
      "signature": "public abstract org.springframework.data.cassandra.domain.Person org.springframework.data.cassandra.repository.aot.PersonRepository.findByFirstname(java.lang.String)",
      "query": {
        "query": "SELECT * FROM person WHERE firstname=?"
      }
    },
    {
      "name": "streamDtoProjectionByFirstname",
      "signature": "public abstract java.util.stream.Stream<org.springframework.data.cassandra.repository.aot.PersonRepository$PersonDto> org.springframework.data.cassandra.repository.aot.PersonRepository.streamDtoProjectionByFirstname(java.lang.String)",
      "query": {
        "query": "SELECT * FROM person WHERE firstname=?"
      }
    },
    {
      "name": "findDeclaredNumberOfChildrenByFirstname",
      "signature": "public abstract int org.springframework.data.cassandra.repository.aot.PersonRepository.findDeclaredNumberOfChildrenByFirstname(java.lang.String)",
      "query": {
        "query": "select numberOfChildren from person where firstname = ?"
      }
    },
    {
      "name": "findWindowByLastname",
      "signature": "public abstract org.springframework.data.domain.Window<org.springframework.data.cassandra.domain.Person> org.springframework.data.cassandra.repository.aot.PersonRepository.findWindowByLastname(java.lang.String,org.springframework.data.domain.ScrollPosition,org.springframework.data.domain.Limit)",
      "query": {
        "query": "SELECT * FROM person WHERE lastname=?"
      }
    }
  ]
}

@mp911de mp911de added this to the 5.0 M6 (2025.1.0) milestone Aug 29, 2025
@mp911de mp911de added type: enhancement A general enhancement theme: aot An issue related to Ahead-Of-Time processing labels Aug 29, 2025
@mp911de mp911de linked an issue Aug 29, 2025 that may be closed by this pull request

return switch (like.getType()) {

case CONTAINING -> "\"%\" + " + parameterName + " + \"%\"";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there a need to escape the value of the parameter?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

expressionString = "#{" + expressionString + "}";
}

builder.add("evaluateExpression($L, $S$L)", context.getExpressionMarker().enclosingMethod(), expressionString,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having a CodeBlock that concats $S with $L given the provided arguments makes me think parameterNames should maybe be represented differently. Right now it seems to render a magic switch that separates arguments used to call a specific variant of the evaluateExpressionMethod making this piece pretty hard to understand.


if (StringUtils.hasText(context.getDynamicProjectionParameterName())) {

builder.addStatement("$1T<$2T> $3L = $4L.query($5L).as($6L)",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need to use index numbers when there're no repeating arguments. doesn't harm having them.

} else if (query.isExists()) {
terminatingMethod = "count() > 0";
} else if (query.isLimited()) {
terminatingMethod = "firstValue()";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does a limited query only return the first available result? Not too familiar with the cassandra domain, but shouldn't this be like a limit to a certain number of results at most (aka TopN results)?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isLimited is a fallback. TopN for collection queries is handled through .isCollectionQuery() a few lines above. This instance here is for User findFirst(…) while User findTop10(…) could work as well.

…ts, and building statement fragments from collections providing argument inputs.
Comment on lines +256 to +259
it.addAll(query, ".and(", (criteria, builder) -> {
builder.add("$1T.where($2S)", Criteria.class, criteria.getColumnName().toCql());
appendPredicate(criteria, builder);
builder.add(")");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addAll seems to be a nice shortcut for appending multiple parts at once.
I wonder if readability suffers a bit from it, like having the closing bracket being added in a nested builder is confusing.


boolean first = true;
for (Sort.Order order : sort) {
invocation.arguments(sort, (order, builder) -> {
Copy link
Member

@christophstrobl christophstrobl Sep 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would it make sense to split the argument from the rendering? thinking of invocation.addArgument(sort).as((argument) -> ... return ) where we do not pass in the builder but have a function that returns a new code block.

as might event be optional if the argument can be rendered as is using $L.

mp911de pushed a commit that referenced this pull request Sep 12, 2025
Closes: #1566
Original pull request: #1600
mp911de added a commit that referenced this pull request Sep 12, 2025
See: #1566
Original pull request: #1600
@mp911de mp911de closed this Sep 12, 2025
@mp911de mp911de deleted the issue/GH-1566 branch September 12, 2025 07:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

theme: aot An issue related to Ahead-Of-Time processing type: enhancement A general enhancement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add support for Cassandra AOT Repositories

3 participants