This is an automated email from the ASF dual-hosted git repository.
paulk 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 d37b245c46 GROOVY-5373: Sql DataSet fails to work with non-literals in
queries (enhancement required)
d37b245c46 is described below
commit d37b245c46ae073c131ae916b7becaaf27762b4d
Author: Paul King <[email protected]>
AuthorDate: Sat Apr 11 19:56:06 2026 +1000
GROOVY-5373: Sql DataSet fails to work with non-literals in queries
(enhancement required)
---
.../src/main/java/groovy/sql/DataSet.java | 8 +++---
.../src/main/java/groovy/sql/SqlWhereVisitor.java | 29 +++++++++++++++++++++-
.../src/test/groovy/groovy/sql/PersonTest.groovy | 19 +++++++++-----
3 files changed, 46 insertions(+), 10 deletions(-)
diff --git a/subprojects/groovy-sql/src/main/java/groovy/sql/DataSet.java
b/subprojects/groovy-sql/src/main/java/groovy/sql/DataSet.java
index 42b6017651..1d1f03b0a0 100644
--- a/subprojects/groovy-sql/src/main/java/groovy/sql/DataSet.java
+++ b/subprojects/groovy-sql/src/main/java/groovy/sql/DataSet.java
@@ -67,9 +67,10 @@ import java.util.Set;
* }
* </pre>
* Currently, the Groovy source code for any accessed POGO must be on the
- * classpath at runtime. Also, at the moment, the expressions (or nested
expressions) can only contain
- * references to fields of the POGO or literals (i.e. constant Strings or
numbers). This limitation
- * may be removed in a future version of Groovy.
+ * classpath at runtime. The expressions (or nested expressions) can contain
+ * references to fields of the POGO, literals (i.e. constant Strings or
numbers),
+ * and variables captured from the enclosing scope. Method calls, arithmetic,
+ * and other complex expressions are not currently supported.
*/
public class DataSet extends Sql {
@@ -414,6 +415,7 @@ public class DataSet extends Sql {
protected SqlWhereVisitor getSqlWhereVisitor() {
if (visitor == null) {
visitor = new SqlWhereVisitor();
+ visitor.setClosure(where);
visit(where, visitor);
}
return visitor;
diff --git
a/subprojects/groovy-sql/src/main/java/groovy/sql/SqlWhereVisitor.java
b/subprojects/groovy-sql/src/main/java/groovy/sql/SqlWhereVisitor.java
index 848926df78..abab0dbc30 100644
--- a/subprojects/groovy-sql/src/main/java/groovy/sql/SqlWhereVisitor.java
+++ b/subprojects/groovy-sql/src/main/java/groovy/sql/SqlWhereVisitor.java
@@ -18,6 +18,7 @@
*/
package groovy.sql;
+import groovy.lang.Closure;
import groovy.lang.GroovyRuntimeException;
import org.codehaus.groovy.ast.CodeVisitorSupport;
import org.codehaus.groovy.ast.expr.BinaryExpression;
@@ -37,6 +38,7 @@ public class SqlWhereVisitor extends CodeVisitorSupport {
private final StringBuffer buffer = new StringBuffer();
private final List<Object> parameters = new ArrayList<Object>();
+ private Closure<?> closure;
public String getWhere() {
return buffer.toString();
@@ -81,9 +83,34 @@ public class SqlWhereVisitor extends CodeVisitorSupport {
buffer.append(expression.getPropertyAsString());
}
+ public void setClosure(Closure<?> closure) {
+ this.closure = closure;
+ }
+
@Override
public void visitVariableExpression(VariableExpression expression) {
- throw new GroovyRuntimeException("DataSet currently doesn't support
arbitrary variables, only literals: found attempted reference to variable '" +
expression.getName() + "'");
+ // Try to resolve captured variables from the closure's context
+ if (closure != null) {
+ String name = expression.getName();
+ try {
+ java.lang.reflect.Field field =
closure.getClass().getDeclaredField(name);
+ if (!field.trySetAccessible()) {
+ throw new GroovyRuntimeException("DataSet unable to access
captured variable '" + name + "'");
+ }
+ Object value = field.get(closure);
+ // Groovy wraps shared (mutable) variables in a Reference
+ if (value instanceof groovy.lang.Reference) {
+ value = ((groovy.lang.Reference<?>) value).get();
+ }
+ getParameters().add(value);
+ buffer.append("?");
+ return;
+ } catch (ReflectiveOperationException ignored) {
+ // fall through to error
+ }
+ }
+ throw new GroovyRuntimeException("DataSet unable to resolve variable
'" + expression.getName()
+ + "'. Supported: literals and variables captured from the
enclosing scope.");
}
public List<Object> getParameters() {
diff --git
a/subprojects/groovy-sql/src/test/groovy/groovy/sql/PersonTest.groovy
b/subprojects/groovy-sql/src/test/groovy/groovy/sql/PersonTest.groovy
index 806966da0a..1d85842318 100644
--- a/subprojects/groovy-sql/src/test/groovy/groovy/sql/PersonTest.groovy
+++ b/subprojects/groovy-sql/src/test/groovy/groovy/sql/PersonTest.groovy
@@ -78,13 +78,20 @@ order by firstName DESC, age'''
assertSql(complexBlogs, expectedSql, expectedParams)
}
- // GROOVY-5371 can be removed once GROOVY-5375 is completed and this is
supported
- void testNonLiteralExpressionsCurrentlyNotSupported() {
+ // GROOVY-5373: variables from enclosing scope are now supported
+ void testVariablesFromEnclosingScope() {
def cutoff = 10
- def message = shouldFail {
- people.findAll { it.size < cutoff && it.lastName == "Bloggs"
}.rows()
- }
- assert message.contains("DataSet currently doesn't support arbitrary
variables, only literals")
+ def blogs = people.findAll { it.size < cutoff && it.lastName ==
"Bloggs" }
+ assertSql(blogs, "select * from person where ((size < ?) and lastName
= ?)", [cutoff, 'Bloggs'])
+ }
+
+ // GROOVY-5373: shared (mutable) captured variables unwrapped from
Reference
+ void testSharedVariableFromEnclosingScope() {
+ def cutoff = 10
+ def blogs = people.findAll { it.size < cutoff && it.lastName ==
"Bloggs" }
+ cutoff = 20 // modification after capture makes cutoff a shared
Reference variable
+ // Reference resolves to current value (20) at query time, not capture
time (10)
+ assertSql(blogs, "select * from person where ((size < ?) and lastName
= ?)", [20, 'Bloggs'])
}
void testDataSetSourceNotAvailable() {