From 86a54bbcf4fdfee56ec5c0317e53d6f4a6dad507 Mon Sep 17 00:00:00 2001 From: Radovan Radic Date: Tue, 26 Nov 2024 14:57:07 +0100 Subject: [PATCH] Fix SELECT FOR UPDATE method matching in raw queries (#3249) * Fix SELECT FOR UPDATE method matching in raw queries * Use correct JpaSpecificationExecutor and add processor test for RawQueryMethodMatcher * Try to improve regex * Simple regex for FOR UPDATE * Try to make Sonar happy with the regex used to match FOR UPDATE * Simplified FOR UPDATE matching regex --- .../data/hibernate/BookRepository.java | 5 ++++- .../finders/RawQueryMethodMatcher.java | 5 ++++- .../data/processor/sql/BuildQuerySpec.groovy | 21 ++++++++++++++----- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/data-hibernate-jpa/src/test/java/io/micronaut/data/hibernate/BookRepository.java b/data-hibernate-jpa/src/test/java/io/micronaut/data/hibernate/BookRepository.java index 7c9cc476126..088b163a072 100644 --- a/data-hibernate-jpa/src/test/java/io/micronaut/data/hibernate/BookRepository.java +++ b/data-hibernate-jpa/src/test/java/io/micronaut/data/hibernate/BookRepository.java @@ -41,6 +41,9 @@ public BookRepository(AuthorRepository authorRepository) { super(authorRepository); } + @Override + public abstract Book save(Book book); + /** * @deprecated Order by 'author.name' case without a join. Hibernate will do the cross join if the association property is accessed by the property path without join. */ @@ -71,7 +74,7 @@ public BookRepository(AuthorRepository authorRepository) { @Query(value = "select count(*) from book b where b.title like :title and b.total_pages > :pages", nativeQuery = true) abstract int countNativeByTitleWithPagesGreaterThan(String title, int pages); - @Query(value = "select * from book where (CASE WHEN CAST(:arg0 AS VARCHAR) is not null THEN title = :arg0 ELSE true END)", nativeQuery = true) + @Query(value = "select * from book where (CASE WHEN CAST(:arg0 AS VARCHAR) is not null THEN title = :arg0 ELSE true END) FOR UPDATE", nativeQuery = true) public abstract List listNativeBooksNullableSearch(@Nullable String arg0); @Query(value = "select * from book where (CASE WHEN exists ( select (:arg0) ) THEN title IN (:arg0) ELSE true END)", nativeQuery = true) diff --git a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/RawQueryMethodMatcher.java b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/RawQueryMethodMatcher.java index 4450b924b85..08777ad3901 100644 --- a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/RawQueryMethodMatcher.java +++ b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/RawQueryMethodMatcher.java @@ -62,6 +62,7 @@ public class RawQueryMethodMatcher implements MethodMatcher { private static final Pattern UPDATE_PATTERN = Pattern.compile(".*\\bupdate\\b.*"); + private static final Pattern FOR_UPDATE_PATTERN = Pattern.compile("for\\s+update"); private static final Pattern DELETE_PATTERN = Pattern.compile(".*\\bdelete\\b.*"); private static final Pattern INSERT_PATTERN = Pattern.compile(".*\\binsert\\b.*"); private static final Pattern RETURNING_PATTERN = Pattern.compile(".*\\breturning\\b.*"); @@ -185,7 +186,9 @@ private DataMethod.OperationType findOperationType(String methodName, String que if (DeleteMethodMatcher.METHOD_PATTERN.matcher(methodName.toLowerCase(Locale.ENGLISH)).matches()) { return DataMethod.OperationType.DELETE; } - return DataMethod.OperationType.UPDATE; + if (!FOR_UPDATE_PATTERN.matcher(query).find()) { + return DataMethod.OperationType.UPDATE; + } } else if (INSERT_PATTERN.matcher(query).find()) { if (RETURNING_PATTERN.matcher(query).find()) { return DataMethod.OperationType.INSERT_RETURNING; diff --git a/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildQuerySpec.groovy b/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildQuerySpec.groovy index b0da47088e1..2a92bf95d30 100644 --- a/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildQuerySpec.groovy +++ b/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildQuerySpec.groovy @@ -2088,7 +2088,7 @@ 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) +@JdbcRepository(dialect = Dialect.MYSQL) @io.micronaut.context.annotation.Executable interface PersonRepository extends CrudRepository { @@ -2098,14 +2098,25 @@ interface PersonRepository extends CrudRepository { \""") $type customSelect(Long id); + @Query(\""" + SELECT * FROM person WHERE id = :id FOR + UPDATE + \""") + $type selectForUpdate(Long id); + } """) - def method = repository.findPossibleMethods("customSelect").findFirst().get() - def selectQuery = getQuery(method) + def customSelectMethod = repository.findPossibleMethods("customSelect").findFirst().get() + def customSelectQuery = getQuery(customSelectMethod) + def selectForUpdateMethod = repository.findPossibleMethods("selectForUpdate").findFirst().get() + def selectForUpdateQuery = getQuery(selectForUpdateMethod) expect: - selectQuery.replace('\n', ' ') == "WITH ids AS (SELECT id FROM person) SELECT * FROM person " - method.classValue(DataMethod, "interceptor").get() == interceptor + customSelectQuery.replace('\n', ' ') == "WITH ids AS (SELECT id FROM person) SELECT * FROM person " + customSelectMethod.classValue(DataMethod, "interceptor").get() == interceptor + + selectForUpdateQuery.replace('\n', ' ') == "SELECT * FROM person WHERE id = :id FOR UPDATE " + selectForUpdateMethod.classValue(DataMethod, "interceptor").get() == interceptor where: type | interceptor