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() {

Reply via email to