This is an automated email from the ASF dual-hosted git repository.

andy pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/jena.git

commit 69e0ae2248b29970198cc5366ec7b5c96549f889
Author: Andy Seaborne <[email protected]>
AuthorDate: Sun Jan 28 14:42:15 2024 +0000

    GH-2218: Implement LATERAL as per SEP-0006/SEP-0007
---
 .../org/apache/jena/sparql/core/Substitute.java    |  35 ++-
 .../org/apache/jena/sparql/core/VarExprList.java   |   8 +-
 .../apache/jena/sparql/engine/binding/Binding.java |   4 +
 .../jena/sparql/engine/binding/BindingBase.java    |  12 +-
 .../sparql/engine/iterator/QueryIterLateral.java   | 303 ++++++++++++++++++++-
 .../jena/sparql/engine/main/JoinClassifier.java    |   7 +-
 .../sparql/engine/main/LeftJoinClassifier.java     |   3 +
 .../jena/sparql/engine/ref/EvaluatorDispatch.java  | 154 +++++------
 .../org/apache/jena/sparql/engine/ref/RefEval.java |   2 +-
 .../apache/jena/sparql/lang/SyntaxVarScope.java    |  53 ++--
 jena-arq/testing/ARQ/Lateral/data2.ttl             |   9 +
 .../testing/ARQ/Lateral/lateral-in-optional.arq    |  15 +
 .../testing/ARQ/Lateral/lateral-in-optional.srj    |  22 ++
 jena-arq/testing/ARQ/Lateral/manifest.ttl          |  20 ++
 .../testing/ARQ/Lateral/optional-in-lateral.arq    |  16 ++
 .../testing/ARQ/Lateral/optional-in-lateral.srj    |  27 ++
 16 files changed, 565 insertions(+), 125 deletions(-)

diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/core/Substitute.java 
b/jena-arq/src/main/java/org/apache/jena/sparql/core/Substitute.java
index 1f7970560f..19cbadc296 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/core/Substitute.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/core/Substitute.java
@@ -20,16 +20,19 @@ package org.apache.jena.sparql.core;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Set;
 
 import org.apache.jena.graph.Node;
 import org.apache.jena.graph.NodeFactory;
 import org.apache.jena.graph.Triple;
 import org.apache.jena.sparql.algebra.Op;
+import org.apache.jena.sparql.algebra.Transform;
 import org.apache.jena.sparql.algebra.TransformCopy;
 import org.apache.jena.sparql.algebra.Transformer;
 import org.apache.jena.sparql.algebra.op.*;
 import org.apache.jena.sparql.engine.binding.Binding;
 import org.apache.jena.sparql.engine.binding.BindingFactory;
+import org.apache.jena.sparql.engine.iterator.QueryIterLateral;
 import org.apache.jena.sparql.expr.Expr;
 import org.apache.jena.sparql.expr.ExprList;
 import org.apache.jena.sparql.pfunction.PropFuncArg;
@@ -40,12 +43,42 @@ import 
org.apache.jena.sparql.syntax.syntaxtransform.UpdateTransformOps;
  * Substitution in SPARQL algebra.
  * <p>
  * See also {@link QueryTransformOps} and {@link UpdateTransformOps} which 
operate on SPARQL syntax.
+ * <p>
+ * {@link #inject} provides the substitution, while leaving a variable 
present, used by LATERAL.
  */
 public class Substitute {
+    /**
+     * Inject takes an {@link Op} to transform using a {Binding binding}. The
+     * transformation assumes the Ope structure is legal for the operation. The
+     * transformation is to wrap each place a variable is used (BGP, GRAPH, 
Path and
+     * some equivalent operations) with a {@code BIND} to restrict the 
vartibale to a specific value
+     * while still retaining the variable (e.g for FILETERs).
+     * <p>
+     * <pre>
+     *   (bgp
+     *     (?s :p 123)
+     *     (?s :q ?a)
+     *   )
+     * </pre>
+     * with binding {@code ?s = :x } becomes
+     * <pre>
+     * (assign (?s :x)
+     *    (bgp
+     *      (:x :p 123)
+     *      (:x :q ?a)
+     *    ))
+     * </pre>
+     */
+    public static Op inject(Op opInput, Binding binding) {
+        Set<Var> injectVars = binding.varsMentioned();
+        Transform transform = new QueryIterLateral.TransformInject(injectVars, 
binding::get);
+        Op opOutput = Transformer.transform(transform, opInput);
+        return opOutput;
+    }
+
     public static Op substitute(Op op, Binding binding) {
         // Want to avoid cost if the binding is empty
         // but the empty test is not zero-cost on non-empty things.
-
         if ( isNotNeeded(binding) )
             return op;
         return Transformer.transform(new OpSubstituteWorker(binding), op);
diff --git 
a/jena-arq/src/main/java/org/apache/jena/sparql/core/VarExprList.java 
b/jena-arq/src/main/java/org/apache/jena/sparql/core/VarExprList.java
index 26756d9027..f2b4336d83 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/core/VarExprList.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/core/VarExprList.java
@@ -35,10 +35,10 @@ public class VarExprList {
     private List<Var> vars;
     private LinkedHashMap<Var, Expr> exprs;   // Preserve order.
 
-    public VarExprList(List<Var> vars) {
-        this.vars = new ArrayList<>(vars);
-        this.exprs = new LinkedHashMap<>();
-    }
+//    public VarExprList(List<Var> vars) {
+//        this.vars = new ArrayList<>(vars);
+//        this.exprs = new LinkedHashMap<>();
+//    }
 
     public VarExprList(VarExprList other) {
         this.vars = new ArrayList<>(other.vars);
diff --git 
a/jena-arq/src/main/java/org/apache/jena/sparql/engine/binding/Binding.java 
b/jena-arq/src/main/java/org/apache/jena/sparql/engine/binding/Binding.java
index c3fc2b044d..9bb7fcf4b7 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/engine/binding/Binding.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/engine/binding/Binding.java
@@ -19,6 +19,7 @@
 package org.apache.jena.sparql.engine.binding;
 
 import java.util.Iterator;
+import java.util.Set;
 import java.util.function.BiConsumer;
 
 import org.apache.jena.graph.Node;
@@ -56,6 +57,9 @@ public interface Binding
     /** Iterate over all variables of this binding. */
     public Iterator<Var> vars();
 
+    /** Iterate over all variables of this binding. */
+    public Set<Var> varsMentioned();
+
     /** Operate on each entry. */
     public void forEach(BiConsumer<Var, Node> action);
 
diff --git 
a/jena-arq/src/main/java/org/apache/jena/sparql/engine/binding/BindingBase.java 
b/jena-arq/src/main/java/org/apache/jena/sparql/engine/binding/BindingBase.java
index 4716290bae..0e7fe54610 100644
--- 
a/jena-arq/src/main/java/org/apache/jena/sparql/engine/binding/BindingBase.java
+++ 
b/jena-arq/src/main/java/org/apache/jena/sparql/engine/binding/BindingBase.java
@@ -19,6 +19,8 @@
 package org.apache.jena.sparql.engine.binding;
 
 import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.Set;
 import java.util.function.BiConsumer;
 
 import org.apache.jena.atlas.iterator.IteratorConcat;
@@ -45,14 +47,20 @@ abstract public class BindingBase implements Binding
 //    public Binding getParent() { return parent; }
 
     @Override
-    final public Iterator<Var> vars()
-    {
+    final public Iterator<Var> vars() {
         Iterator<Var> iter = vars1();
         if ( parent != null )
             iter = IteratorConcat.concat(parent.vars(), iter);
         return iter;
     }
 
+    @Override
+    final public Set<Var> varsMentioned() {
+        Set<Var> result = new LinkedHashSet<>();
+        vars().forEachRemaining(result::add);
+        return result;
+    }
+
     /** Operate on each entry. */
     @Override
     public void forEach(BiConsumer<Var, Node> action) {
diff --git 
a/jena-arq/src/main/java/org/apache/jena/sparql/engine/iterator/QueryIterLateral.java
 
b/jena-arq/src/main/java/org/apache/jena/sparql/engine/iterator/QueryIterLateral.java
index 734c9278a5..1162c6cb80 100644
--- 
a/jena-arq/src/main/java/org/apache/jena/sparql/engine/iterator/QueryIterLateral.java
+++ 
b/jena-arq/src/main/java/org/apache/jena/sparql/engine/iterator/QueryIterLateral.java
@@ -18,23 +18,36 @@
 
 package org.apache.jena.sparql.engine.iterator;
 
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Function;
+
+import org.apache.jena.atlas.lib.SetUtils;
+import org.apache.jena.graph.Node;
+import org.apache.jena.graph.Triple;
 import org.apache.jena.sparql.algebra.Op;
-import org.apache.jena.sparql.algebra.op.OpTable;
-import org.apache.jena.sparql.core.Substitute;
+import org.apache.jena.sparql.algebra.TransformCopy;
+import org.apache.jena.sparql.algebra.op.*;
+import org.apache.jena.sparql.core.*;
 import org.apache.jena.sparql.engine.ExecutionContext;
 import org.apache.jena.sparql.engine.QueryIterator;
 import org.apache.jena.sparql.engine.binding.Binding;
+import org.apache.jena.sparql.engine.binding.BindingBuilder;
+import org.apache.jena.sparql.engine.binding.BindingFactory;
 import org.apache.jena.sparql.engine.main.QC;
+import org.apache.jena.sparql.expr.NodeValue;
+import org.apache.jena.sparql.util.VarUtils;
 
 public class QueryIterLateral extends QueryIterRepeatApply {
 
-    private final Op lateralOp;
+    private final Op subOp;
     private final boolean isUnit;
 
-    public QueryIterLateral(QueryIterator input, Op lateralOp, 
ExecutionContext execCxt) {
+    public QueryIterLateral(QueryIterator input, Op subOp, ExecutionContext 
execCxt) {
         super(input, execCxt);
-        this.lateralOp = lateralOp;
-        this.isUnit = isJoinIdentity(lateralOp);
+        this.subOp = subOp;
+        this.isUnit = isJoinIdentity(subOp);
     }
 
     private boolean isJoinIdentity(Op op) {
@@ -47,7 +60,283 @@ public class QueryIterLateral extends QueryIterRepeatApply {
     protected QueryIterator nextStage(Binding binding) {
         if ( isUnit )
             return QueryIterSingleton.create(binding, super.getExecContext());
-        Op op = Substitute.substitute(lateralOp, binding);
+        Op op = Substitute.inject(subOp, binding);
         return QC.execute(op, binding, super.getExecContext());
     }
+
+    /*
+     * This transform applies variable substitution by injecting a binding for 
the variable and
+     * also substituting the variable in the op.
+     * The second step - substitution - is strictly unnecessary it happens 
anyway.
+     */
+    public static class TransformInject extends TransformCopy {
+
+        private final Set<Var> injectVars;
+        private final Set<Node> varsAsNodes;
+        private final Function<Var, Node> replacement;
+        private static final boolean substitute = true;
+
+        // Replacement becomes binding.??
+        // Or "op call injection"!!
+        public TransformInject(Set<Var> injectVars, Function<Var, Node> 
replacement) {
+            this.injectVars = injectVars;
+            this.varsAsNodes = Set.copyOf(injectVars);
+            this.replacement = replacement;
+        }
+
+        @Override
+        public Op transform(OpBGP opBGP) {
+            if ( injectVars.isEmpty())
+                return opBGP;
+            BasicPattern bp = opBGP.getPattern();
+            List<Triple> triples = bp.getList();
+            // Alt - build up triple by triple.
+                //generateAssignmentTriple(triple, assigns, builder);
+            Set<Var> bgpVars = new LinkedHashSet<>();
+            VarUtils.addVarsTriples(bgpVars, triples);
+            Set<Var> x = SetUtils.intersection(bgpVars, injectVars);
+            if ( x.isEmpty())
+                return opBGP;
+            VarExprList assigns = new VarExprList();
+            BindingBuilder builder = BindingFactory.builder();
+            for ( Var var : x )
+                generateAssignmentVar(var, assigns, builder);
+
+            if ( assigns.isEmpty() )
+                return super.transform(opBGP);
+
+            // If no substitutions, do less work.
+            Binding substitutions = builder.build();
+            Op opExec = substitute
+                    ? Substitute.substitute(opBGP, substitutions)
+                    : opBGP;
+            opExec = OpAssign.create(opExec, assigns);
+            return opExec;
+        }
+
+//        @Override
+//        public Op transform(OpQuadBlock opQuadBlock) {
+//            opQuadBlock.getPattern();
+//        }
+
+        @Override
+        public Op transform(OpQuadPattern opQuadPattern) {
+            // "One node : Commonality with OpGraph, OpService (OpDatasetNames)
+            // Function for maker.
+            BasicPattern bgp = opQuadPattern.getBasicPattern();
+            VarExprList assigns = new VarExprList();
+            BindingBuilder builder = BindingFactory.builder();
+            Node gn = opQuadPattern.getGraphNode();
+
+            generateAssignmentNode(gn, assigns, builder);
+
+            for ( Triple t : bgp ) {
+                generateAssignmentTriple(t, assigns, builder);
+            }
+            if ( assigns.isEmpty() )
+                return super.transform(opQuadPattern);
+            // If no substitutions, do less work.
+            Op opExec = opQuadPattern;
+            if ( substitute ) {
+                Binding substitutions = builder.build();
+                opExec = Substitute.substitute(opQuadPattern, substitutions);
+            }
+            opExec = OpAssign.create(opExec, assigns);
+            return opExec;
+        }
+
+        // XXX Extract - do one node
+
+        @Override
+        public Op transform(OpService opService, Op subOp) {
+            Node g = opService.getService();
+            if ( ! workToDo(g) )
+                return super.transform(opService, subOp);
+            // Fast small lists?
+            VarExprList assigns = new VarExprList();
+            BindingBuilder builder = BindingFactory.builder();
+            Var var = Var.alloc(g);
+            generateAssignmentVar(var, assigns, builder);
+            if ( assigns.isEmpty() )
+                return super.transform(opService, subOp);
+            // subOp has already been processed.
+
+            Node g2 = g;
+            Op op2;
+            if ( substitute ) {
+                Binding substitutions = builder.build();
+                g2 = substitutions.get(var);
+                op2 = new OpService(g2, subOp, opService.getSilent());
+            } else
+                op2 = new OpService(g, subOp, opService.getSilent());
+            Op opExec = OpAssign.create(op2, assigns);
+            return opExec;
+        }
+
+        @Override
+        public Op transform(OpGraph opGraph, Op subOp) {
+            Node g = opGraph.getNode();
+            if ( ! workToDo(g) )
+                return super.transform(opGraph, subOp);
+            // Fast small lists?
+            VarExprList assigns = new VarExprList();
+            BindingBuilder builder = BindingFactory.builder();
+            Var var = Var.alloc(g);
+            generateAssignmentVar(var, assigns, builder);
+            if ( assigns.isEmpty() )
+                return super.transform(opGraph, subOp);
+            // subOp has already been processed.
+
+            Node g2 = g;
+            Op op2;
+            if ( substitute ) {
+                Binding substitutions = builder.build();
+                g2 = substitutions.get(var);
+                op2 = new OpGraph(g2, subOp);
+            } else
+                op2 = new OpGraph(g, subOp);
+            Op opExec = OpAssign.create(op2, assigns);
+            return opExec;
+        }
+
+        @Override
+        public Op transform(OpDatasetNames opDatasetNames) {
+            Node g = opDatasetNames.getGraphNode();
+            if ( ! workToDo(g) )
+                return super.transform(opDatasetNames);
+            Var var = Var.alloc(g);
+            Node g2 = replacement.apply(var);
+            Op op2 = new OpGraph(g2, OpTable.unit());
+            return op2;
+        }
+
+//        Binding for variables occurs in several places in SPARQL:
+    //
+//            Basic Graph Pattern Matching
+//            Property Path Patterns
+//            evaluation of algebra form Graph(var,P) involving a variable 
(from the syntax GRAPH ?variable {&hellip;})
+
+        @Override
+        public Op transform(OpPath opPath) {
+            // Exactly one of predicate and path is defined.
+            TriplePath path = opPath.getTriplePath();
+            VarExprList assigns = new VarExprList();
+            BindingBuilder builder = BindingFactory.builder();
+
+            if ( path.isTriple() ) {
+                Triple t1 = path.asTriple();
+                generateAssignmentTriple(t1, assigns, builder);
+                // XXX Triple t2 = Substitute.substitute(triple, binding);
+                Triple t2 = applyReplacement(t1, replacement);
+                if ( t1.equals(t2) )
+                    return opPath;
+                TriplePath path2 = new TriplePath(t2);
+                return new OpPath(path2);
+            }
+
+            // Path, not predicate.
+            Node s = path.getSubject();
+            Node o = path.getObject();
+            if ( ! workToDo(s) && ! workToDo(o) )
+                return super.transform(opPath);
+
+            generateAssignmentNode(s, assigns, builder);
+            generateAssignmentNode(o, assigns, builder);
+
+            if ( assigns.isEmpty() )
+                return super.transform(opPath);
+
+            Node s2 = applyReplacement(s, replacement);
+            Node o2 = applyReplacement(o, replacement);
+            TriplePath path2 = new TriplePath(s2, path.getPath(), o2);
+            Op op2 = new OpPath(path2);
+            Op opExec = OpAssign.create(op2, assigns);
+            return opExec;
+        }
+
+        @Override
+        public Op transform(OpTriple opTriple) {
+            Triple triple = opTriple.getTriple();
+            VarExprList assigns = new VarExprList();
+            BindingBuilder builder = BindingFactory.builder();
+            generateAssignmentTriple(triple, assigns, builder);
+            if ( assigns.isEmpty() )
+                return super.transform(opTriple);
+            Triple t2 = triple;
+            if ( substitute )
+                t2 = applyReplacement(triple, replacement);
+            Op op2 = new OpTriple(t2);
+            Op opExec = OpAssign.create(op2, assigns);
+            return opExec;
+        }
+
+        private Triple applyReplacement(Triple triple, Function<Var, Node> 
replacement) {
+            Node s2 = applyReplacement(triple.getSubject(), replacement);
+            Node p2 = applyReplacement(triple.getPredicate(), replacement);
+            Node o2 = applyReplacement(triple.getObject(), replacement);
+            Triple t2 = Triple.create(s2, p2, o2);
+            return t2;
+        }
+
+        private void generateAssignmentTriple(Triple triple, VarExprList 
assigns, BindingBuilder builder) {
+            Node s = triple.getSubject();
+            Node p = triple.getPredicate();
+            Node o = triple.getObject();
+            if ( ! workToDo(s) && ! workToDo(p) && ! workToDo(o) )
+                return;
+            generateAssignmentNode(s, assigns, builder);
+            generateAssignmentNode(p, assigns, builder);
+            generateAssignmentNode(o, assigns, builder);
+        }
+
+        private static Node applyReplacement(Node n, Function<Var, Node> 
replacement) {
+            if ( n instanceof Var x ) {
+                Node x2 = replacement.apply(x);
+                return ( x2 == null ) ? n : x2;
+            }
+            return n;
+    }
+
+//        // Allow for nulls.
+//        private Triple transform(Triple triple) {
+//
+//            if ( assigns.isEmpty() )
+//                return triple;
+//
+//            // XXX assignments()
+//
+//            Triple triple2 = Triple.create(s, p, o);
+//            return triple2;
+//        }
+
+        // XXX Other, non-std, ops.
+
+        private void generateAssignmentNode(Node n, VarExprList assigns, 
BindingBuilder builder) {
+            if ( n == null )
+                return;
+            if ( ! Var.isVar(n) )
+                return;
+            generateAssignmentVar(Var.alloc(n), assigns, builder);
+        }
+
+        private void generateAssignmentVar(Var var, VarExprList assigns, 
BindingBuilder builder) {
+            Node value = replacement.apply(var);
+            if ( value != null ) {
+                if ( ! builder.contains(var) ) {
+                    builder.add(var, value);
+                    assigns.add(var, NodeValue.makeNode(value));
+                }
+            }
+        }
+
+        // XXX Needed? To avoid object allocation.
+        private boolean workToDo(Node n) {
+            if ( n == null )
+                return false;
+            if ( ! Var.isVar(n) )
+                return false;
+            Var v = Var.alloc(n);
+            return null != replacement.apply(v);
+        }
+    }
 }
diff --git 
a/jena-arq/src/main/java/org/apache/jena/sparql/engine/main/JoinClassifier.java 
b/jena-arq/src/main/java/org/apache/jena/sparql/engine/main/JoinClassifier.java
index 194654085a..2855857b43 100644
--- 
a/jena-arq/src/main/java/org/apache/jena/sparql/engine/main/JoinClassifier.java
+++ 
b/jena-arq/src/main/java/org/apache/jena/sparql/engine/main/JoinClassifier.java
@@ -60,6 +60,9 @@ public class JoinClassifier
         if ( right instanceof OpTopN )      return false ;
         if ( right instanceof OpOrder )     return false ;
 
+        // Lateral is different.
+        if ( right instanceof OpLateral )   return false ;
+
         // Assume something will not commute these later on.
         return check(left, right) ;
     }
@@ -67,7 +70,9 @@ public class JoinClassifier
     // -- pre check for ops we can't handle in a linear fashion.
     // These are the negation patterns (minus and diff)
     // FILTER NOT EXISTS is safe - it's defined by iteration like the linear 
execution algorithm.
-    private static class UnsafeLineraOpException extends RuntimeException {}
+    private static class UnsafeLineraOpException extends RuntimeException {
+        @Override public Throwable fillInStackTrace() { return this; }
+    }
     private static OpVisitor checkForUnsafeVisitor = new OpVisitorBase() {
         @Override public void visit(OpMinus opMinus) { throw new 
UnsafeLineraOpException(); }
         @Override public void visit(OpDiff opDiff)   { throw new 
UnsafeLineraOpException(); }
diff --git 
a/jena-arq/src/main/java/org/apache/jena/sparql/engine/main/LeftJoinClassifier.java
 
b/jena-arq/src/main/java/org/apache/jena/sparql/engine/main/LeftJoinClassifier.java
index b94a601315..85c59069e2 100644
--- 
a/jena-arq/src/main/java/org/apache/jena/sparql/engine/main/LeftJoinClassifier.java
+++ 
b/jena-arq/src/main/java/org/apache/jena/sparql/engine/main/LeftJoinClassifier.java
@@ -25,6 +25,7 @@ import org.apache.jena.atlas.lib.SetUtils ;
 import org.apache.jena.sparql.algebra.Op ;
 import org.apache.jena.sparql.algebra.OpVars ;
 import org.apache.jena.sparql.algebra.op.OpExt ;
+import org.apache.jena.sparql.algebra.op.OpLateral;
 import org.apache.jena.sparql.algebra.op.OpLeftJoin ;
 import org.apache.jena.sparql.algebra.op.OpModifier ;
 import org.apache.jena.sparql.core.Var ;
@@ -59,6 +60,8 @@ public class LeftJoinClassifier
         // With SELECT *, it's as if the subquery were just the pattern.
         if ( right instanceof OpModifier )
             return false ;
+        if ( right instanceof OpLateral )
+            return false ;
 
         Set<Var> leftVars = OpVars.visibleVars(left) ;
         if ( print ) {
diff --git 
a/jena-arq/src/main/java/org/apache/jena/sparql/engine/ref/EvaluatorDispatch.java
 
b/jena-arq/src/main/java/org/apache/jena/sparql/engine/ref/EvaluatorDispatch.java
index 1df12f683b..f1b1de2790 100644
--- 
a/jena-arq/src/main/java/org/apache/jena/sparql/engine/ref/EvaluatorDispatch.java
+++ 
b/jena-arq/src/main/java/org/apache/jena/sparql/engine/ref/EvaluatorDispatch.java
@@ -35,88 +35,76 @@ import org.apache.jena.sparql.exec.http.Service;
 
 /**  Class to provide type-safe eval() dispatch using the visitor support of 
Op */
 
-public class EvaluatorDispatch implements OpVisitor
-{
+public class EvaluatorDispatch implements OpVisitor {
     private Deque<Table> stack = new ArrayDeque<>();
     protected Evaluator evaluator;
 
-    public EvaluatorDispatch(Evaluator evaluator)
-    {
+    public EvaluatorDispatch(Evaluator evaluator) {
         this.evaluator = evaluator;
     }
 
-    protected Table eval(Op op)
-    {
+    protected Table eval(Op op) {
         op.visit(this);
         return pop();
     }
 
-    Table getResult()
-    {
+    Table getResult() {
         if ( stack.size() != 1 )
-            Log.warn(this, "Warning: getResult: stack size = "+stack.size());
+            Log.warn(this, "Warning: getResult: stack size = " + stack.size());
 
         Table table = pop();
         return table;
     }
 
     @Override
-    public void visit(OpBGP opBGP)
-    {
+    public void visit(OpBGP opBGP) {
         Table table = evaluator.basicPattern(opBGP.getPattern());
         push(table);
     }
 
     @Override
-    public void visit(OpQuadPattern quadPattern)
-    {
+    public void visit(OpQuadPattern quadPattern) {
         push(RefEval.evalQuadPattern(quadPattern, evaluator));
     }
 
     @Override
-    public void visit(OpQuadBlock quadBlock)
-    {
+    public void visit(OpQuadBlock quadBlock) {
         push(eval(quadBlock.convertOp()));
-        //push(Eval.evalQuadPattern(quadBlock, evaluator));
+        // push(Eval.evalQuadPattern(quadBlock, evaluator));
     }
 
     @Override
-    public void visit(OpTriple opTriple)
-    {
+    public void visit(OpTriple opTriple) {
         visit(opTriple.asBGP());
     }
 
     @Override
-    public void visit(OpQuad opQuad)
-    {
+    public void visit(OpQuad opQuad) {
         visit(opQuad.asQuadPattern());
     }
+
     @Override
-    public void visit(OpPath opPath)
-    {
+    public void visit(OpPath opPath) {
         Table table = evaluator.pathPattern(opPath.getTriplePath());
         push(table);
     }
 
     @Override
-    public void visit(OpProcedure opProc)
-    {
+    public void visit(OpProcedure opProc) {
         Table table = eval(opProc.getSubOp());
         table = evaluator.procedure(table, opProc.getProcId(), 
opProc.getArgs());
         push(table);
     }
 
     @Override
-    public void visit(OpPropFunc opPropFunc)
-    {
+    public void visit(OpPropFunc opPropFunc) {
         Table table = eval(opPropFunc.getSubOp());
         table = evaluator.propertyFunction(table, opPropFunc.getProperty(), 
opPropFunc.getSubjectArgs(), opPropFunc.getObjectArgs());
         push(table);
     }
 
     @Override
-    public void visit(OpJoin opJoin)
-    {
+    public void visit(OpJoin opJoin) {
         Table left = eval(opJoin.getLeft());
         Table right = eval(opJoin.getRight());
         Table table = evaluator.join(left, right);
@@ -124,13 +112,11 @@ public class EvaluatorDispatch implements OpVisitor
     }
 
     @Override
-    public void visit(OpSequence opSequence)
-    {
+    public void visit(OpSequence opSequence) {
         // Evaluation is as a sequence of joins.
         Table table = TableFactory.createUnit();
 
-        for ( Iterator<Op> iter = opSequence.iterator(); iter.hasNext(); )
-        {
+        for ( Iterator<Op> iter = opSequence.iterator() ; iter.hasNext() ; ) {
             Op op = iter.next();
             Table eltTable = eval(op);
             table = evaluator.join(table, eltTable);
@@ -139,13 +125,11 @@ public class EvaluatorDispatch implements OpVisitor
     }
 
     @Override
-    public void visit(OpDisjunction opDisjunction)
-    {
+    public void visit(OpDisjunction opDisjunction) {
         // Evaluation is as a concatentation of alternatives
         Table table = TableFactory.createEmpty();
 
-        for ( Iterator<Op> iter = opDisjunction.iterator(); iter.hasNext(); )
-        {
+        for ( Iterator<Op> iter = opDisjunction.iterator() ; iter.hasNext() ; 
) {
             Op op = iter.next();
             Table eltTable = eval(op);
             table = evaluator.union(table, eltTable);
@@ -154,17 +138,26 @@ public class EvaluatorDispatch implements OpVisitor
     }
 
     @Override
-    public void visit(OpLeftJoin opLeftJoin)
-    {
+    public void visit(OpLeftJoin opLeftJoin) {
         Table left = eval(opLeftJoin.getLeft());
         Table right = eval(opLeftJoin.getRight());
+
+        try {
+            left.rows();
+        } catch (Throwable th) {
+            Op x = opLeftJoin.getLeft();
+            eval(x);
+
+            System.out.println("NULL");
+        }
+
+
         Table table = evaluator.leftJoin(left, right, opLeftJoin.getExprs());
         push(table);
     }
 
     @Override
-    public void visit(OpDiff opDiff)
-    {
+    public void visit(OpDiff opDiff) {
         Table left = eval(opDiff.getLeft());
         Table right = eval(opDiff.getRight());
         Table table = evaluator.diff(left, right);
@@ -172,8 +165,7 @@ public class EvaluatorDispatch implements OpVisitor
     }
 
     @Override
-    public void visit(OpMinus opMinus)
-    {
+    public void visit(OpMinus opMinus) {
         Table left = eval(opMinus.getLeft());
         Table right = eval(opMinus.getRight());
         Table table = evaluator.minus(left, right);
@@ -181,8 +173,7 @@ public class EvaluatorDispatch implements OpVisitor
     }
 
     @Override
-    public void visit(OpUnion opUnion)
-    {
+    public void visit(OpUnion opUnion) {
         Table left = eval(opUnion.getLeft());
         Table right = eval(opUnion.getRight());
         Table table = evaluator.union(left, right);
@@ -190,8 +181,7 @@ public class EvaluatorDispatch implements OpVisitor
     }
 
     @Override
-    public void visit(OpConditional opCond)
-    {
+    public void visit(OpConditional opCond) {
         Table left = eval(opCond.getLeft());
         // Ref engine - don't care about efficiency
         Table right = eval(opCond.getRight());
@@ -200,60 +190,53 @@ public class EvaluatorDispatch implements OpVisitor
     }
 
     @Override
-    public void visit(OpLateral opLateral)
-    {
+    public void visit(OpLateral opLateral) {
         Table left = eval(opLateral.getLeft());
         Table table = evaluator.lateral(left, opLateral.getRight());
         push(table);
     }
 
     @Override
-    public void visit(OpFilter opFilter)
-    {
+    public void visit(OpFilter opFilter) {
         Table table = eval(opFilter.getSubOp());
         table = evaluator.filter(opFilter.getExprs(), table);
         push(table);
     }
 
     @Override
-    public void visit(OpGraph opGraph)
-    {
+    public void visit(OpGraph opGraph) {
         push(RefEval.evalGraph(opGraph, evaluator));
     }
 
     @Override
-    public void visit(OpService opService)
-    {
+    public void visit(OpService opService) {
         QueryIterator qIter = Service.exec(opService, ARQ.getContext());
         Table table = TableFactory.create(qIter);
         push(table);
     }
 
     @Override
-    public void visit(OpDatasetNames dsNames)
-    {
+    public void visit(OpDatasetNames dsNames) {
         push(RefEval.evalDS(dsNames, evaluator));
     }
 
     @Override
-    public void visit(OpTable opTable)
-    {
+    public void visit(OpTable opTable) {
         push(opTable.getTable());
     }
 
     @Override
-    public void visit(OpExt opExt)
-    { throw new QueryExecException("Encountered OpExt during execution of 
reference engine"); }
+    public void visit(OpExt opExt) {
+        throw new QueryExecException("Encountered OpExt during execution of 
reference engine");
+    }
 
     @Override
-    public void visit(OpNull opNull)
-    {
+    public void visit(OpNull opNull) {
         push(TableFactory.createEmpty());
     }
 
     @Override
-    public void visit(OpLabel opLabel)
-    {
+    public void visit(OpLabel opLabel) {
         if ( opLabel.hasSubOp() )
             push(eval(opLabel.getSubOp()));
         else
@@ -262,93 +245,86 @@ public class EvaluatorDispatch implements OpVisitor
     }
 
     @Override
-    public void visit(OpList opList)
-    {
+    public void visit(OpList opList) {
         Table table = eval(opList.getSubOp());
         table = evaluator.list(table);
         push(table);
     }
 
     @Override
-    public void visit(OpOrder opOrder)
-    {
+    public void visit(OpOrder opOrder) {
         Table table = eval(opOrder.getSubOp());
         table = evaluator.order(table, opOrder.getConditions());
         push(table);
     }
 
     @Override
-    public void visit(OpTopN opTop)
-    {
+    public void visit(OpTopN opTop) {
         Table table = eval(opTop.getSubOp());
-        //table = evaluator.topN(table, opTop.getLimti(), 
opTop.getConditions());
+        // table = evaluator.topN(table, opTop.getLimti(), 
opTop.getConditions());
         table = evaluator.order(table, opTop.getConditions());
         table = evaluator.slice(table, 0, opTop.getLimit());
         push(table);
     }
 
     @Override
-    public void visit(OpProject opProject)
-    {
+    public void visit(OpProject opProject) {
         Table table = eval(opProject.getSubOp());
         table = evaluator.project(table, opProject.getVars());
         push(table);
     }
 
     @Override
-    public void visit(OpDistinct opDistinct)
-    {
+    public void visit(OpDistinct opDistinct) {
         Table table = eval(opDistinct.getSubOp());
         table = evaluator.distinct(table);
         push(table);
     }
 
     @Override
-    public void visit(OpReduced opReduced)
-    {
+    public void visit(OpReduced opReduced) {
         Table table = eval(opReduced.getSubOp());
         table = evaluator.reduced(table);
         push(table);
     }
 
     @Override
-    public void visit(OpSlice opSlice)
-    {
+    public void visit(OpSlice opSlice) {
         Table table = eval(opSlice.getSubOp());
         table = evaluator.slice(table, opSlice.getStart(), 
opSlice.getLength());
         push(table);
     }
 
     @Override
-    public void visit(OpAssign opAssign)
-    {
+    public void visit(OpAssign opAssign) {
         Table table = eval(opAssign.getSubOp());
         table = evaluator.assign(table, opAssign.getVarExprList());
         push(table);
     }
 
     @Override
-    public void visit(OpExtend opExtend)
-    {
+    public void visit(OpExtend opExtend) {
         Table table = eval(opExtend.getSubOp());
         table = evaluator.extend(table, opExtend.getVarExprList());
         push(table);
     }
 
     @Override
-    public void visit(OpGroup opGroup)
-    {
+    public void visit(OpGroup opGroup) {
         Table table = eval(opGroup.getSubOp());
         table = evaluator.groupBy(table, opGroup.getGroupVars(), 
opGroup.getAggregators());
         push(table);
     }
 
-    protected void push(Table table)  { stack.push(table); }
-    protected Table pop()
-    {
+    protected void push(Table table) {
+        stack.push(table);
+    }
+
+    protected Table pop() {
         if ( stack.size() == 0 )
             Log.warn(this, "Warning: pop: empty stack");
-        return stack.pop();
+        Table table = stack.pop();
+        return table;
     }
 
 }
diff --git 
a/jena-arq/src/main/java/org/apache/jena/sparql/engine/ref/RefEval.java 
b/jena-arq/src/main/java/org/apache/jena/sparql/engine/ref/RefEval.java
index ba212ff3f8..83ddfd4256 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/engine/ref/RefEval.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/engine/ref/RefEval.java
@@ -49,7 +49,6 @@ import 
org.apache.jena.sparql.engine.iterator.QueryIterPlainWrapper;
 import org.apache.jena.sparql.engine.iterator.QueryIterRoot;
 import org.apache.jena.sparql.engine.main.QC;
 
-// Spit out a few of the longer ops.
 public class RefEval {
     public static Table eval(Evaluator evaluator, Op op) {
         EvaluatorDispatch ev = new EvaluatorDispatch(evaluator);
@@ -58,6 +57,7 @@ public class RefEval {
         return table;
     }
 
+    // Spit out a few of the longer ops.
     static Table evalDS(OpDatasetNames opDSN, Evaluator evaluator) {
         Node graphNode = opDSN.getGraphNode();
         if ( graphNode.isURI() ) {
diff --git 
a/jena-arq/src/main/java/org/apache/jena/sparql/lang/SyntaxVarScope.java 
b/jena-arq/src/main/java/org/apache/jena/sparql/lang/SyntaxVarScope.java
index cb79fd01ce..5e0690ad15 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/lang/SyntaxVarScope.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/lang/SyntaxVarScope.java
@@ -31,21 +31,29 @@ import org.apache.jena.sparql.syntax.*;
 
 /** Calculate in-scope variables from the AST */
 public class SyntaxVarScope {
+    // @formatter:off
     /* SPARQL 1.1 "in scope" rules These define the variables from a pattern 
that are
      * in-scope These are not the usage rules.
      *
-     * Syntax Form In-scope variables
+     * <a href="https://www.w3.org/TR/sparql11-query/#variableScope";>18.2.1 
Variable Scope</a>
      *
-     * Basic Graph Pattern (BGP) v occurs in the BGP Path v occurs in the path 
Group
-     * { P1 P2 ... } v is in-scope if in-scope in one or more of P1, P2, ... 
GRAPH
-     * term { P } v is term or v is in-scope in P { P1 } UNION { P2 } v is 
in-scope
-     * in P1 or in-scope in P2 OPTIONAL {P} v is in-scope in P SERVICE term 
{P} v is
-     * term or v is in-scope in P (expr AS v) for BIND, SELECT and GROUP BY v 
is
-     * in-scope SELECT ..v .. { P } v is in-scope if v is mentioned as a 
project
-     * variable SELECT * { P } v is in-scope in P VALUES var (values) v is 
in-scope
-     * if v is in varlist VALUES varlist (values) v is in-scope if v is in 
varlist */
-
-    // Weakness : EXISTS inside FILTERs?
+     * Syntax Form In-scope variables
+     * <pre>
+     * Basic Graph Pattern (BGP) v occurs in the BGP
+     * Path                      v occurs in the path
+     * Group { P1 P2 ... }       v is in-scope if in-scope in one or more of 
P1, P2, ...
+     * GRAPH term { P }          v is term or v is in-scope in P
+     * { P1 } UNION { P2 }       v is in-scope in P1 or in-scope in P2
+     * OPTIONAL {P}              v is in-scope in P
+     * SERVICE term {P}          v is term or v is in-scope in P
+     * (expr AS v) for BIND, SELECT and GROUP BY   v is in-scope
+     * SELECT ..v .. { P }       v is in-scope if v is mentioned as a project 
variable
+     * SELECT * { P }            v is in-scope in P
+     * VALUES var (values)       v is in-scope
+     * VALUES varlist (values)   v is in-scope if v is in varlist
+     * </pre>
+     */
+    //@formatter:on
 
     public static void check(Query query) {
         if ( query.getQueryPattern() == null )
@@ -220,17 +228,17 @@ public class SyntaxVarScope {
             for ( int i = 0 ; i < el.size() ; i++ ) {
                 Element e = el.get(i);
                 // Tests.
-                if ( e instanceof ElementBind ) {
+                if ( e instanceof ElementBind eltBind) {
                     Collection<Var> accScope = calcScopeAll(el.getElements(), 
i);
-                    checkBIND(accScope, (ElementBind)e);
+                    checkBIND(accScope, eltBind);
                 }
-                if ( e instanceof ElementService ) {
+                if ( e instanceof ElementService eltSvc ) {
                     Collection<Var> accScope = calcScopeAll(el.getElements(), 
i);
-                    checkSERVICE(accScope, (ElementService)e);
+                    checkSERVICE(accScope, eltSvc);
                 }
-                if ( e instanceof ElementLateral ) {
+                if ( e instanceof ElementLateral eltLat) {
                     Collection<Var> accScope = calcScopeAll(el.getElements(), 
i);
-                    checkLATERAL(accScope, e);
+                    checkLATERAL(accScope, eltLat);
                 }
             }
         }
@@ -285,6 +293,7 @@ public class SyntaxVarScope {
 
                 @Override
                 public void visit(ElementSubQuery eltSubQuery) {
+
                     // Only called when there is an expression.
                     eltSubQuery.getQuery().getProject().forEachExpr((var, 
expr) -> {
                         if ( accScope.contains(var) )
@@ -293,19 +302,23 @@ public class SyntaxVarScope {
                     // Check inside query pattern
                     Query subQuery = eltSubQuery.getQuery();
                     Collection<Var> accScope2 = accScope;
-                    //eltSubQuery.getQuery().setResultVars();
                     if ( ! subQuery.isQueryResultStar() ) {
                         List<Var> projectVars = 
eltSubQuery.getQuery().getProject().getVars();
-                        // Calculate variables passed down : scope which are 
in the project vars.
+                        // Copy
                         accScope2 = new ArrayList<>(accScope);
+                        // Calculate variables passed down : remove any that 
are not in the project vars
+                        // Any reused name will be renamed apart later.
                         accScope2.removeIf(v->!projectVars.contains(v));
                     }
+                    if ( accScope2.isEmpty() )
+                        // No work to do.
+                        return;
                     Element el2 = eltSubQuery.getQuery().getQueryPattern();
                     checkLATERAL(accScope2, el2);
                 }
             };
 
-            // Does not walk into subqueries but we need to change the scoep 
for sub-queries.
+            // Does not walk into subqueries but we need to change the scope 
for sub-queries.
             ElementWalker.walk(el, checker);
         }
     }
diff --git a/jena-arq/testing/ARQ/Lateral/data2.ttl 
b/jena-arq/testing/ARQ/Lateral/data2.ttl
new file mode 100644
index 0000000000..35745fbbfa
--- /dev/null
+++ b/jena-arq/testing/ARQ/Lateral/data2.ttl
@@ -0,0 +1,9 @@
+@prefix ex: <http://example.org/> .
+
+ex:s1 a ex:T ;
+   ex:p "11" , "12" , "13" .
+
+ex:s2 a ex:T ;
+   ex:p "21" , "22" , "23" .
+
+ex:s3 a ex:T .
diff --git a/jena-arq/testing/ARQ/Lateral/lateral-in-optional.arq 
b/jena-arq/testing/ARQ/Lateral/lateral-in-optional.arq
new file mode 100644
index 0000000000..a746707a5d
--- /dev/null
+++ b/jena-arq/testing/ARQ/Lateral/lateral-in-optional.arq
@@ -0,0 +1,15 @@
+  PREFIX  ex:   <http://example.org/>
+
+  SELECT  ?s ?o
+  WHERE
+    { ?s  a  ex:T
+      OPTIONAL
+        { LATERAL
+            { SELECT  ?s ?o
+              WHERE
+                { ?s  ex:p  ?o }
+              ORDER BY ?o
+              LIMIT   2
+            }
+        }
+    }
\ No newline at end of file
diff --git a/jena-arq/testing/ARQ/Lateral/lateral-in-optional.srj 
b/jena-arq/testing/ARQ/Lateral/lateral-in-optional.srj
new file mode 100644
index 0000000000..1a521b0769
--- /dev/null
+++ b/jena-arq/testing/ARQ/Lateral/lateral-in-optional.srj
@@ -0,0 +1,22 @@
+{ "head": {
+    "vars": [ "s" , "o" ]
+  } ,
+  "results": {
+    "bindings": [
+      { 
+        "s": { "type": "uri" , "value": "http://example.org/s3"; }
+      } ,
+      { 
+        "s": { "type": "uri" , "value": "http://example.org/s1"; } ,
+        "o": { "type": "literal" , "value": "11" }
+      } ,
+      { 
+        "s": { "type": "uri" , "value": "http://example.org/s1"; } ,
+        "o": { "type": "literal" , "value": "12" }
+      } ,
+      { 
+        "s": { "type": "uri" , "value": "http://example.org/s2"; }
+      }
+    ]
+  }
+}
diff --git a/jena-arq/testing/ARQ/Lateral/manifest.ttl 
b/jena-arq/testing/ARQ/Lateral/manifest.ttl
index 08276f22a1..ea2b7ee3a0 100644
--- a/jena-arq/testing/ARQ/Lateral/manifest.ttl
+++ b/jena-arq/testing/ARQ/Lateral/manifest.ttl
@@ -37,6 +37,8 @@ PREFIX skos:    <http://www.w3.org/2004/02/skos/core#>
       <#lateral-3>
       <#lateral-4>
       <#lateral-5>
+      <#lateral-6>
+      <#lateral-7>
     ).
 
 <#lateral-1> rdf:type mf:QueryEvaluationTest ;
@@ -84,6 +86,24 @@ PREFIX skos:    <http://www.w3.org/2004/02/skos/core#>
     mf:result  <lateral-5.srj>
     .
 
+<#lateral-6> rdf:type mf:QueryEvaluationTest ;
+    mf:name      "LATERAL - LATERAL inside OPTIONAL" ;
+    mf:action [
+       qt:query  <lateral-in-optional.arq> ;
+       qt:data   <data2.ttl>
+    ] ;
+    mf:result  <lateral-in-optional.srj>
+    .
+
+<#lateral-7> rdf:type mf:QueryEvaluationTest ;
+    mf:name      "LATERAL - OPTIONAL inside LATERAL" ;
+    mf:action [
+       qt:query  <optional-in-lateral.arq> ;
+       qt:data   <data2.ttl>
+    ] ;
+    mf:result  <optional-in-lateral.srj>
+    .
+
 
 ## ?x :q ?z . LATERAL { ?s :q ?o FILTER (?z = ?o) }
 
diff --git a/jena-arq/testing/ARQ/Lateral/optional-in-lateral.arq 
b/jena-arq/testing/ARQ/Lateral/optional-in-lateral.arq
new file mode 100644
index 0000000000..19834d9ee7
--- /dev/null
+++ b/jena-arq/testing/ARQ/Lateral/optional-in-lateral.arq
@@ -0,0 +1,16 @@
+  PREFIX  ex:   <http://example.org/>
+
+  SELECT  ?s ?o
+  WHERE
+    { ?s  a  ex:T
+      LATERAL
+        { OPTIONAL
+            { SELECT  ?s ?o
+              WHERE
+                { ?s  ex:p  ?o }
+              ORDER BY ?o
+              LIMIT   2
+            }
+        }
+    }
+    
\ No newline at end of file
diff --git a/jena-arq/testing/ARQ/Lateral/optional-in-lateral.srj 
b/jena-arq/testing/ARQ/Lateral/optional-in-lateral.srj
new file mode 100644
index 0000000000..97d1bd12c8
--- /dev/null
+++ b/jena-arq/testing/ARQ/Lateral/optional-in-lateral.srj
@@ -0,0 +1,27 @@
+{ "head": {
+    "vars": [ "s" , "o" ]
+  } ,
+  "results": {
+    "bindings": [
+      { 
+        "s": { "type": "uri" , "value": "http://example.org/s3"; }
+      } ,
+      { 
+        "s": { "type": "uri" , "value": "http://example.org/s1"; } ,
+        "o": { "type": "literal" , "value": "11" }
+      } ,
+      { 
+        "s": { "type": "uri" , "value": "http://example.org/s1"; } ,
+        "o": { "type": "literal" , "value": "12" }
+      } ,
+      { 
+        "s": { "type": "uri" , "value": "http://example.org/s2"; } ,
+        "o": { "type": "literal" , "value": "21" }
+      } ,
+      { 
+        "s": { "type": "uri" , "value": "http://example.org/s2"; } ,
+        "o": { "type": "literal" , "value": "22" }
+      }
+    ]
+  }
+}


Reply via email to