diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PreprocessedQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PreprocessedQuery.java index 715aeb214e..58d381e1b1 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PreprocessedQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PreprocessedQuery.java @@ -58,6 +58,7 @@ * * @author Christoph Strobl * @author Mark Paluch + * @author Nabil Fawwaz Elqayyim * @since 4.0 */ public final class PreprocessedQuery implements DeclaredQuery { @@ -247,6 +248,7 @@ PreprocessedQuery parse(String query, Function declaredQu String resultingQuery = parsedQuery.getQueryString(); Matcher matcher = PARAMETER_BINDING_PATTERN.matcher(resultingQuery); + java.util.BitSet commentPositions = findCommentPositions(resultingQuery); ParameterBindings parameterBindings = new ParameterBindings(bindings, it -> checkAndRegister(it, bindings)); int currentIndex = 0; @@ -255,7 +257,7 @@ PreprocessedQuery parse(String query, Function declaredQu while (matcher.find()) { - if (parsedQuery.isQuoted(matcher.start())) { + if (parsedQuery.isQuoted(matcher.start()) || commentPositions.get(matcher.start())) { continue; } @@ -625,4 +627,68 @@ public boolean hasLabels() { } } + private static java.util.BitSet findCommentPositions(String query) { + int queryLen = query.length(); + java.util.BitSet isComment = new java.util.BitSet(queryLen); + boolean inBlockComment = false; + boolean inLineComment = false; + boolean inString = false; + + for (int i = 0; i < queryLen; i++) { + char c = query.charAt(i); + char next = (i + 1 < queryLen) ? query.charAt(i + 1) : '\0'; + + // String Literal State: Comment markers are treated as normal characters + if (inString) { + if (c == '\'') { + // Handle SQL-style escaped quotes: '' + if (next == '\'') { + i++; + } else { + inString = false; + } + } + continue; + } + + // Block Comment State: Identify positions within /* ... */ + if (inBlockComment) { + isComment.set(i); + if (c == '*' && next == '/') { + isComment.set(i + 1); + inBlockComment = false; + i++; + } + continue; + } + + // Line Comment State: Identify positions within -- or // + if (inLineComment) { + isComment.set(i); + // End line comment state on newline or carriage return + if (c == '\n' || c == '\r') { + inLineComment = false; + } + continue; + } + + // Entering States: Check for quotes or comment markers + if (c == '\'') { + inString = true; + } else if (c == '/' && next == '*') { + // Start of block comment + inBlockComment = true; + isComment.set(i); + isComment.set(i + 1); + i++; + } else if ((c == '-' && next == '-') || (c == '/' && next == '/')) { + // Start of line comment (both SQL-style '--' and Java-style '//') + inLineComment = true; + isComment.set(i); + isComment.set(i + 1); + i++; + } + } + return isComment; + } } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/PreprocessedQueryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/PreprocessedQueryUnitTests.java index d4c9e9d08a..ec212d0e98 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/PreprocessedQueryUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/PreprocessedQueryUnitTests.java @@ -29,6 +29,7 @@ * @author Jens Schauder * @author Diego Krupitza * @author Mark Paluch + * @author Nabil Fawwaz Elqayyim */ class PreprocessedQueryUnitTests { @@ -52,4 +53,40 @@ static Stream parameters() { Arguments.argumentSet("no parameter", "select u from User u where u.email = u.email", false)); } + @ParameterizedTest // GH-4090 + @MethodSource("commentParameters") + void shouldIgnoreMarkersInComments(String queryText, int expectedBindingCount, String description) { + + // Use the official static factory method from the DeclaredQuery interface + DeclaredQuery declaredQuery = DeclaredQuery.jpqlQuery(queryText); + PreprocessedQuery preprocessed = PreprocessedQuery.parse(declaredQuery); + + assertThat(preprocessed.getBindings()) + .as(description) + .hasSize(expectedBindingCount); + } + + static Stream commentParameters() { + return Stream.of( + // 1. Block Comment Scenarios + Arguments.of("SELECT e FROM Entity e /* block ? */ WHERE e.id = :id", 1, "Standard block comment"), + Arguments.of("SELECT e FROM Entity e /* asterisk * inside */ WHERE e.id = :id", 1, "Asterisk inside block comment"), + + // 2. SQL-style Line Comment Scenarios + Arguments.of("SELECT e FROM Entity e -- line comment \n WHERE e.id = :id", 1, "SQL-style line comment with newline"), + + // 3. Java-style Line Comment Scenarios + // This specific case turns the remaining yellow branches green. + Arguments.of("SELECT e FROM Entity e // java comment \r WHERE e.id = :id", 1, "Java-style line comment with carriage return"), + + // 4. Mathematical Operators + // Ensures '/' and '-' are not always treated as comment starts. + Arguments.of("SELECT e FROM Entity e WHERE e.id / 2 = 1 AND e.id * 2 = :id", 1, "Slash and asterisk as operators"), + Arguments.of("SELECT e FROM Entity e WHERE e.id - 1 = :id", 1, "Hyphen as subtraction operator"), + + // 5. String Literal and Escaping Scenarios + // Confirms that markers inside quotes are ignored and escaped quotes are handled. + Arguments.of("SELECT e FROM Entity e WHERE e.name = 'It''s a -- marker' AND e.id = :id", 1, "Markers inside string literals") + ); + } }