Skip to content

Commit

Permalink
Fix parameter substitution for raw queries containing colon(s) (#3282)
Browse files Browse the repository at this point in the history
  • Loading branch information
radovanradic authored Jan 14, 2025
1 parent 0169b20 commit 35f267b
Show file tree
Hide file tree
Showing 4 changed files with 70 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package io.micronaut.data.jdbc.oraclexe

import groovy.transform.Memoized
import io.micronaut.data.tck.entities.Book
import io.micronaut.data.tck.entities.Face
import io.micronaut.data.tck.repositories.*
import io.micronaut.data.tck.tests.AbstractRepositorySpec
import spock.lang.PendingFeature
Expand Down Expand Up @@ -271,4 +272,17 @@ class OracleXERepositorySpec extends AbstractRepositorySpec implements OracleTes
bookRepository.findById(book.id).get().title == "Xyz"
}

void "test native query with colon"() {
given:
def face = faceRepository.save(new Face("New"))
def oracleFaceRepository = (OracleXEFaceRepository) faceRepository
when:
def faces = oracleFaceRepository.findAllWithOptionalFilters(null, "2024-01-01")
then:
faces
faces[0].name == face.name
cleanup:
faceRepository.delete(face)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,25 @@
*/
package io.micronaut.data.jdbc.oraclexe;

import io.micronaut.core.annotation.Nullable;
import io.micronaut.data.annotation.Query;
import io.micronaut.data.jdbc.annotation.JdbcRepository;
import io.micronaut.data.model.query.builder.sql.Dialect;
import io.micronaut.data.tck.entities.Face;
import io.micronaut.data.tck.repositories.FaceRepository;

import java.util.List;

@JdbcRepository(dialect = Dialect.ORACLE)
public interface OracleXEFaceRepository extends FaceRepository {

@Query(
"""
SELECT * FROM face f WHERE
(f.date_created >= COALESCE(TO_TIMESTAMP(:dateCreatedParam, 'YYYY-MM-DD"T"HH24\\:MI\\:SS"Z"'), f.date_created) OR :dateCreatedParam IS NULL) AND
(f.name = :name OR :name IS NULL)
""")
List<Face> findAllWithOptionalFilters(
@Nullable String name,
@Nullable String dateCreatedParam);
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ public class RawQueryMethodMatcher implements MethodMatcher {
private static final Pattern RETURNING_PATTERN = Pattern.compile(".*\\breturning\\b.*");

private static final Pattern VARIABLE_PATTERN = Pattern.compile("([^:\\\\]*)((?<![:]):([a-zA-Z0-9]+))([^:]*)");
private static final String COLON = ":";
private static final String COLON_ESCAPE_PATTERN = "\\" + COLON;
private static final String COLON_TEMP_REPLACEMENT = "___MICRONAUT_COLON_PLA@CEHOLDER___";

@Override
public final int getOrder() {
Expand Down Expand Up @@ -251,7 +254,8 @@ private QueryResult getQueryResult(MethodMatchContext matchContext,
boolean namedParameters,
ParameterElement entityParam,
SourcePersistentEntity persistentEntity) {
Matcher matcher = VARIABLE_PATTERN.matcher(queryString.replace("\\:", ""));
String newQueryString = queryString.replace(COLON_ESCAPE_PATTERN, COLON_TEMP_REPLACEMENT);
Matcher matcher = VARIABLE_PATTERN.matcher(newQueryString);

List<AnnotationValue<ParameterExpression>> parameterExpressions = matchContext.getMethodElement()
.getAnnotationMetadata()
Expand All @@ -262,9 +266,9 @@ private QueryResult getQueryResult(MethodMatchContext matchContext,
int index = 1;
int lastOffset = 0;
while (matcher.find()) {
String prefix = queryString.substring(lastOffset, matcher.start(3) - 1);
String prefix = newQueryString.substring(lastOffset, matcher.start(3) - 1);
if (!prefix.isEmpty()) {
queryParts.add(prefix);
queryParts.add(prefix.replace(COLON_TEMP_REPLACEMENT, COLON));
}
lastOffset = matcher.end(3);
String name = matcher.group(3);
Expand All @@ -285,13 +289,12 @@ private QueryResult getQueryResult(MethodMatchContext matchContext,
parameterBindings.add(queryParameterBinding);
}

queryString = queryString.replace("\\:", ":");
if (queryParts.isEmpty()) {
queryParts.add(queryString);
queryParts.add(newQueryString.replace(COLON_TEMP_REPLACEMENT, COLON));
} else if (lastOffset > 0) {
queryParts.add(queryString.substring(lastOffset));
queryParts.add(newQueryString.substring(lastOffset).replace(COLON_TEMP_REPLACEMENT, COLON));
}
String finalQueryString = queryString;
String finalQueryString = newQueryString.replace(COLON_TEMP_REPLACEMENT, COLON);
return new QueryResult() {
@Override
public String getQuery() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import static io.micronaut.data.processor.visitors.TestUtils.getParameterBinding
import static io.micronaut.data.processor.visitors.TestUtils.getParameterExpressions
import static io.micronaut.data.processor.visitors.TestUtils.getParameterPropertyPaths
import static io.micronaut.data.processor.visitors.TestUtils.getQuery
import static io.micronaut.data.processor.visitors.TestUtils.getQueryParts
import static io.micronaut.data.processor.visitors.TestUtils.getRawQuery
import static io.micronaut.data.processor.visitors.TestUtils.getResultDataType
import static io.micronaut.data.processor.visitors.TestUtils.isExpandableQuery
Expand Down Expand Up @@ -640,6 +641,36 @@ interface FacesRepository extends CrudRepository<Face, Long> {

}

void "test native query with colon used in query"() {
given:
def repository = buildRepository('test.FacesRepository', """
import io.micronaut.core.annotation.Nullable;
import io.micronaut.data.jdbc.annotation.JdbcRepository;
import io.micronaut.data.model.query.builder.sql.Dialect;
import io.micronaut.data.repository.GenericRepository;
import io.micronaut.data.tck.entities.Face;
import java.util.UUID;
@JdbcRepository(dialect = Dialect.ORACLE)
interface FacesRepository extends GenericRepository<Face, Long> {
@Query("SELECT * FROM face f WHERE" +
" (f.date_created >= COALESCE(TO_TIMESTAMP(:dateCreated, 'YYYY-MM-DD\\"T\\"HH24\\\\:MI\\\\:SS\\"Z\\"'), f.date_created) OR :dateCreated IS NULL) AND"
+ " (f.name = :name OR :name IS NULL)")
List<Face> findAllWithOptionalFilters(
@Nullable String name,
@Nullable String dateCreated);
}
""")
def method = repository.getRequiredMethod("findAllWithOptionalFilters", String, String)
def rawQuery = getRawQuery(method)
def query = getQuery(method)

expect:
rawQuery == 'SELECT * FROM face f WHERE (f.date_created >= COALESCE(TO_TIMESTAMP(?, \'YYYY-MM-DD"T"HH24:MI:SS"Z"\'), f.date_created) OR ? IS NULL) AND (f.name = ? OR ? IS NULL)'
query == 'SELECT * FROM face f WHERE (f.date_created >= COALESCE(TO_TIMESTAMP(:dateCreated, \'YYYY-MM-DD"T"HH24\\:MI\\:SS"Z"\'), f.date_created) OR :dateCreated IS NULL) AND (f.name = :name OR :name IS NULL)'
}

void "test In in properties"() {
given:
def repository = buildRepository('test.PurchaseRepository', """
Expand Down

0 comments on commit 35f267b

Please sign in to comment.