Skip to content

Commit

Permalink
Enhance "RETURNING" clause method matching (#3227)
Browse files Browse the repository at this point in the history
This PR fixes an issue where a RETURNING clause was not recognized. When using a text-block to write the explicit query and using a line-break directly before the RETURNING clause, the wrong Interceptor was used in the end when executing the query.
  • Loading branch information
lkavan authored Nov 14, 2024
1 parent e2faafa commit 22713cd
Show file tree
Hide file tree
Showing 3 changed files with 262 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,9 @@ public class RawQueryMethodMatcher implements MethodMatcher {
private static final String SELECT = "select";
private static final String DELETE = "delete";
private static final String UPDATE = "update";
private static final String RETURNING = " returning ";
private static final String INSERT = "insert";

private static final Pattern RETURNING_PATTERN = Pattern.compile(".*\\breturning\\b.*");
private static final Pattern VARIABLE_PATTERN = Pattern.compile("([^:\\\\]*)((?<![:]):([a-zA-Z0-9]+))([^:]*)");

@Override
Expand Down Expand Up @@ -176,20 +176,20 @@ private DataMethod.OperationType findOperationType(String methodName, String que
if (query.startsWith(SELECT)) {
return DataMethod.OperationType.QUERY;
} else if (query.startsWith(DELETE)) {
if (query.contains(RETURNING)) {
if (RETURNING_PATTERN.matcher(query).find()) {
return DataMethod.OperationType.DELETE_RETURNING;
}
return DataMethod.OperationType.DELETE;
} else if (query.startsWith(UPDATE)) {
if (query.contains(RETURNING)) {
if (RETURNING_PATTERN.matcher(query).find()) {
return DataMethod.OperationType.UPDATE_RETURNING;
}
if (DeleteMethodMatcher.METHOD_PATTERN.matcher(methodName.toLowerCase(Locale.ENGLISH)).matches()) {
return DataMethod.OperationType.DELETE;
}
return DataMethod.OperationType.UPDATE;
} else if (query.startsWith(INSERT)) {
if (query.contains(RETURNING)) {
if (RETURNING_PATTERN.matcher(query).find()) {
return DataMethod.OperationType.INSERT_RETURNING;
}
return DataMethod.OperationType.INSERT;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
*/
package io.micronaut.data.processor.sql

import io.micronaut.data.intercept.DeleteReturningManyInterceptor
import io.micronaut.data.intercept.DeleteReturningOneInterceptor
import io.micronaut.data.intercept.annotation.DataMethod
import io.micronaut.data.model.DataType
import io.micronaut.data.processor.visitors.AbstractDataSpec
import spock.lang.Unroll
Expand Down Expand Up @@ -429,4 +432,129 @@ interface AccountRepository extends CrudRepository<Account, Long> {
getResultDataType(deleteById) == null
}

@Unroll
void "test build delete returning for type #type"() {
given:
def repository = buildRepository('test.PersonRepository', """
import io.micronaut.data.jdbc.annotation.JdbcRepository;
import io.micronaut.data.model.query.builder.sql.Dialect;
import io.micronaut.data.tck.entities.Person;
@JdbcRepository(dialect= Dialect.MYSQL)
@io.micronaut.context.annotation.Executable
interface PersonRepository extends CrudRepository<Person, Long> {
@Query("DELETE FROM person RETURNING *")
$type customDeleteReturning(Long id);
}
""")
def method = repository.findPossibleMethods("customDeleteReturning").findFirst().get()
def deleteQuery = getQuery(method)

expect:
deleteQuery == "DELETE FROM person RETURNING *"
method.classValue(DataMethod, "interceptor").get() == interceptor

where:
type | interceptor
'java.util.List<Person>' | DeleteReturningManyInterceptor
'Person' | DeleteReturningOneInterceptor
}

@Unroll
void "test build delete returning for type #type text-block no-indent"() {
given:
def repository = buildRepository('test.PersonRepository', """
import io.micronaut.data.jdbc.annotation.JdbcRepository;
import io.micronaut.data.model.query.builder.sql.Dialect;
import io.micronaut.data.tck.entities.Person;
@JdbcRepository(dialect= Dialect.MYSQL)
@io.micronaut.context.annotation.Executable
interface PersonRepository extends CrudRepository<Person, Long> {
@Query(\"""
DELETE FROM person
RETURNING *
\""")
$type customDeleteReturning(Long id);
}
""")
def method = repository.findPossibleMethods("customDeleteReturning").findFirst().get()
def deleteQuery = getQuery(method)

expect:
deleteQuery.replace('\n', ' ') == "DELETE FROM person RETURNING * "
method.classValue(DataMethod, "interceptor").get() == interceptor

where:
type | interceptor
'java.util.List<Person>' | DeleteReturningManyInterceptor
'Person' | DeleteReturningOneInterceptor
}

@Unroll
void "test build delete returning property for type #type"() {
given:
def repository = buildRepository('test.PersonRepository', """
import io.micronaut.data.jdbc.annotation.JdbcRepository;
import io.micronaut.data.model.query.builder.sql.Dialect;
import io.micronaut.data.tck.entities.Person;
@JdbcRepository(dialect= Dialect.MYSQL)
@io.micronaut.context.annotation.Executable
interface PersonRepository extends CrudRepository<Person, Long> {
@Query("DELETE FROM person RETURNING id")
$type customDeleteReturning(Long id);
}
""")
def method = repository.findPossibleMethods("customDeleteReturning").findFirst().get()
def deleteQuery = getQuery(method)

expect:
deleteQuery == "DELETE FROM person RETURNING id"
method.classValue(DataMethod, "interceptor").get() == interceptor

where:
type | interceptor
'java.util.List<Long>' | DeleteReturningManyInterceptor
'Long' | DeleteReturningOneInterceptor
}

@Unroll
void "test build delete returning property for type #type text-block no-indent"() {
given:
def repository = buildRepository('test.PersonRepository', """
import io.micronaut.data.jdbc.annotation.JdbcRepository;
import io.micronaut.data.model.query.builder.sql.Dialect;
import io.micronaut.data.tck.entities.Person;
@JdbcRepository(dialect= Dialect.MYSQL)
@io.micronaut.context.annotation.Executable
interface PersonRepository extends CrudRepository<Person, Long> {
@Query(\"""
DELETE FROM person
RETURNING id
\""")
$type customDeleteReturning(Long id);
}
""")
def method = repository.findPossibleMethods("customDeleteReturning").findFirst().get()
def deleteQuery = getQuery(method)

expect:
deleteQuery.replace('\n', ' ') == "DELETE FROM person RETURNING id "
method.classValue(DataMethod, "interceptor").get() == interceptor

where:
type | interceptor
'java.util.List<Long>' | DeleteReturningManyInterceptor
'Long' | DeleteReturningOneInterceptor
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ package io.micronaut.data.processor.sql


import io.micronaut.data.intercept.UpdateInterceptor
import io.micronaut.data.intercept.UpdateReturningManyInterceptor
import io.micronaut.data.intercept.UpdateReturningOneInterceptor
import io.micronaut.data.intercept.annotation.DataMethod
import io.micronaut.data.intercept.async.UpdateAsyncInterceptor
import io.micronaut.data.intercept.reactive.UpdateReactiveInterceptor
Expand Down Expand Up @@ -67,6 +69,134 @@ interface PersonRepository extends CrudRepository<Person, Long> {
'void' | UpdateInterceptor
}

@Unroll
void "test build update returning for type #type"() {
given:
def repository = buildRepository('test.PersonRepository', """
import io.micronaut.data.jdbc.annotation.JdbcRepository;
import io.micronaut.data.model.query.builder.sql.Dialect;
import io.micronaut.data.tck.entities.Person;
@JdbcRepository(dialect= Dialect.MYSQL)
@io.micronaut.context.annotation.Executable
interface PersonRepository extends CrudRepository<Person, Long> {
@Query("UPDATE person SET name = 'test' WHERE id = :id RETURNING *")
$type customUpdateReturning(Long id);
}
""")
def method = repository.findPossibleMethods("customUpdateReturning").findFirst().get()
def updateQuery = getQuery(method)

expect:
updateQuery == "UPDATE person SET name = 'test' WHERE id = :id RETURNING *"
method.classValue(DataMethod, "interceptor").get() == interceptor

where:
type | interceptor
'java.util.List<Person>' | UpdateReturningManyInterceptor
'Person' | UpdateReturningOneInterceptor
}

@Unroll
void "test build update returning for type #type text-block no-indent"() {
given:
def repository = buildRepository('test.PersonRepository', """
import io.micronaut.data.jdbc.annotation.JdbcRepository;
import io.micronaut.data.model.query.builder.sql.Dialect;
import io.micronaut.data.tck.entities.Person;
@JdbcRepository(dialect= Dialect.MYSQL)
@io.micronaut.context.annotation.Executable
interface PersonRepository extends CrudRepository<Person, Long> {
@Query(\"""
UPDATE person SET name = 'test'
WHERE id = :id
RETURNING *
\""")
$type customUpdateReturning(Long id);
}
""")
def method = repository.findPossibleMethods("customUpdateReturning").findFirst().get()
def updateQuery = getQuery(method)

expect:
updateQuery.replace('\n', ' ') == "UPDATE person SET name = 'test' WHERE id = :id RETURNING * "
method.classValue(DataMethod, "interceptor").get() == interceptor

where:
type | interceptor
'java.util.List<Person>' | UpdateReturningManyInterceptor
'Person' | UpdateReturningOneInterceptor
}

@Unroll
void "test build update returning property for type #type"() {
given:
def repository = buildRepository('test.PersonRepository', """
import io.micronaut.data.jdbc.annotation.JdbcRepository;
import io.micronaut.data.model.query.builder.sql.Dialect;
import io.micronaut.data.tck.entities.Person;
@JdbcRepository(dialect= Dialect.MYSQL)
@io.micronaut.context.annotation.Executable
interface PersonRepository extends CrudRepository<Person, Long> {
@Query("UPDATE person SET name = 'test' WHERE id = :id RETURNING id")
$type customUpdateReturning(Long id);
}
""")
def method = repository.findPossibleMethods("customUpdateReturning").findFirst().get()
def updateQuery = getQuery(method)

expect:
updateQuery == "UPDATE person SET name = 'test' WHERE id = :id RETURNING id"
method.classValue(DataMethod, "interceptor").get() == interceptor

where:
type | interceptor
'java.util.List<Long>' | UpdateReturningManyInterceptor
'Long' | UpdateReturningOneInterceptor
}

@Unroll
void "test build update returning property for type #type text-block no-indent"() {
given:
def repository = buildRepository('test.PersonRepository', """
import io.micronaut.data.jdbc.annotation.JdbcRepository;
import io.micronaut.data.model.query.builder.sql.Dialect;
import io.micronaut.data.tck.entities.Person;
@JdbcRepository(dialect= Dialect.MYSQL)
@io.micronaut.context.annotation.Executable
interface PersonRepository extends CrudRepository<Person, Long> {
@Query(\"""
UPDATE person SET name = 'test'
WHERE id = :id
RETURNING id
\""")
$type customUpdateReturning(Long id);
}
""")
def method = repository.findPossibleMethods("customUpdateReturning").findFirst().get()
def updateQuery = getQuery(method)

expect:
updateQuery.replace('\n', ' ') == "UPDATE person SET name = 'test' WHERE id = :id RETURNING id "
method.classValue(DataMethod, "interceptor").get() == interceptor

where:
type | interceptor
'java.util.List<Long>' | UpdateReturningManyInterceptor
'Long' | UpdateReturningOneInterceptor
}

@Unroll
void "test build update with datasource set"() {
given:
Expand Down

0 comments on commit 22713cd

Please sign in to comment.