You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by pa...@apache.org on 2026/09/04 06:41:20 UTC
(groovy) branch master updated: GROOVY-12350: Handle ?? escapes in SQL InListExpander
This is an automated email from the ASF dual-hosted git repository.
paulk-asert pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git
The following commit(s) were added to refs/heads/master by this push:
new 8d6d4d7086 GROOVY-12350: Handle ?? escapes in SQL InListExpander
8d6d4d7086 is described below
commit 8d6d4d7086e190e94ad654685e318d9f14780be4
Author: Paul King <pa...@asert.com.au>
AuthorDate: Fri Sep 4 14:58:25 2026 +1000
GROOVY-12350: Handle ?? escapes in SQL InListExpander
---
.../src/main/java/groovy/sql/InListExpander.java | 13 +++++-
.../groovy/groovy/sql/InListExpanderTest.groovy | 50 ++++++++++++++++++++++
2 files changed, 62 insertions(+), 1 deletion(-)
diff --git a/subprojects/groovy-sql/src/main/java/groovy/sql/InListExpander.java b/subprojects/groovy-sql/src/main/java/groovy/sql/InListExpander.java
index 221dda149a..0753175d7a 100644
--- a/subprojects/groovy-sql/src/main/java/groovy/sql/InListExpander.java
+++ b/subprojects/groovy-sql/src/main/java/groovy/sql/InListExpander.java
@@ -31,7 +31,8 @@ import java.util.List;
* <p>
* The SQL is walked character-by-character so that {@code ?} characters
* appearing inside single-quoted strings, double-quoted identifiers, line
- * comments, and block comments are ignored.
+ * comments, block comments, and a doubled {@code ??} operator escape are
+ * ignored.
* <p>
* Package-private; consumed by {@link Sql} prior to {@code prepareStatement}.
*/
@@ -82,6 +83,16 @@ final class InListExpander {
}
break;
case '?':
+ // A doubled '?' is how a driver escapes a literal '?' operator — PostgreSQL's
+ // json/hstore operators ? ?| ?& collide with the placeholder, so pgjdbc reads
+ // '??' as one of them and binds nothing to it. Two adjacent placeholders are
+ // not valid SQL in any case, so there is nothing else this can mean. Passing it
+ // through untouched leaves the driver to unescape it.
+ if (i + 1 < len && sql.charAt(i + 1) == '?') {
+ out.append("??");
+ i += 2;
+ break;
+ }
if (paramIdx < params.size() && params.get(paramIdx) instanceof InListParameter) {
Collection<?> values = ((InListParameter) params.get(paramIdx)).getValues();
// Sql.inList(...) enforces non-empty, but the interface is public
diff --git a/subprojects/groovy-sql/src/test/groovy/groovy/sql/InListExpanderTest.groovy b/subprojects/groovy-sql/src/test/groovy/groovy/sql/InListExpanderTest.groovy
index 4eb14ef528..82ed3cd3d8 100644
--- a/subprojects/groovy-sql/src/test/groovy/groovy/sql/InListExpanderTest.groovy
+++ b/subprojects/groovy-sql/src/test/groovy/groovy/sql/InListExpanderTest.groovy
@@ -60,6 +60,56 @@ class InListExpanderTest {
assert result.params == [42]
}
+ @Test
+ void doubledQuestionMarkIsAnOperatorNotAPlaceholder() {
+ // PostgreSQL's driver takes ?? as an escaped literal ? operator, for the JSON
+ // operators ? ?| ?&, so it consumes no parameter
+ def result = InListExpander.expand(
+ "SELECT * FROM t WHERE data ?? 'k' AND id IN (?)",
+ [Sql.inList([1, 2, 3])])
+ assert result.sql == "SELECT * FROM t WHERE data ?? 'k' AND id IN (?, ?, ?)"
+ assert result.params == [1, 2, 3]
+ }
+
+ @Test
+ void escapedOperatorLeavesLaterParametersAligned() {
+ def result = InListExpander.expand(
+ "SELECT * FROM t WHERE data ?? 'k' AND id IN (?) AND active = ?",
+ [Sql.inList([1, 2]), true])
+ assert result.sql == "SELECT * FROM t WHERE data ?? 'k' AND id IN (?, ?) AND active = ?"
+ assert result.params == [1, 2, true]
+ }
+
+ @Test
+ void escapedFormsOfTheOtherJsonOperatorsArePreserved() {
+ // ?| and ?& escape as ??| and ??&; the list has two values so that an expansion
+ // landing on the operator instead of the marker is visible
+ def result = InListExpander.expand(
+ "SELECT * FROM t WHERE tags ??| ARRAY['a'] AND keys ??& ARRAY['b'] AND id IN (?)",
+ [Sql.inList([7, 8])])
+ assert result.sql == "SELECT * FROM t WHERE tags ??| ARRAY['a'] AND keys ??& ARRAY['b'] AND id IN (?, ?)"
+ assert result.params == [7, 8]
+ }
+
+ @Test
+ void anOddRunOfQuestionMarksIsAnEscapeThenAPlaceholder() {
+ // a list has to be present or expand returns before the walk begins
+ def result = InListExpander.expand(
+ 'SELECT * FROM t WHERE data ??? AND id IN (?)',
+ ['x', Sql.inList([1, 2])])
+ assert result.sql == 'SELECT * FROM t WHERE data ??? AND id IN (?, ?)'
+ assert result.params == ['x', 1, 2]
+ }
+
+ @Test
+ void doubledQuestionMarkInsideAStringIsStillJustText() {
+ def result = InListExpander.expand(
+ "SELECT * FROM t WHERE note = '??' AND id IN (?)",
+ [Sql.inList([1, 2])])
+ assert result.sql == "SELECT * FROM t WHERE note = '??' AND id IN (?, ?)"
+ assert result.params == [1, 2]
+ }
+
@Test
void questionMarkInsideSingleQuotedStringIsNotExpanded() {
def result = InListExpander.expand(