diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/algebra/Algebra.java b/jena-arq/src/main/java/org/apache/jena/sparql/algebra/Algebra.java index 72f5d87f3a1..af4bf49fa98 100644 --- a/jena-arq/src/main/java/org/apache/jena/sparql/algebra/Algebra.java +++ b/jena-arq/src/main/java/org/apache/jena/sparql/algebra/Algebra.java @@ -161,22 +161,27 @@ public static Binding merge(Binding bindingLeft, Binding bindingRight) { // If compatible, merge. Iterate over variables in right but not in left. BindingBuilder b = Binding.builder(bindingLeft); - for ( Iterator vIter = bindingRight.vars() ; vIter.hasNext() ; ) { - Var v = vIter.next(); - Node n = bindingRight.get(v); + bindingRight.forEach((v, n) -> { if ( !bindingLeft.contains(v) ) b.add(v, n); - } + }); return b.build(); } public static boolean compatible(Binding bindingLeft, Binding bindingRight) { // Test to see if compatible: Iterate over variables in left - for ( Iterator vIter = bindingLeft.vars() ; vIter.hasNext() ; ) { - Var v = vIter.next(); + return compatible(bindingLeft, bindingRight, bindingLeft.vars()); + } + + /** Test to see if bindings are compatible for all variables of the provided iterator. */ + public static boolean compatible(Binding bindingLeft, Binding bindingRight, Iterator vars) { + while (vars.hasNext() ) { + Var v = vars.next(); Node nLeft = bindingLeft.get(v); - Node nRight = bindingRight.get(v); + if ( nLeft == null ) + continue; + Node nRight = bindingRight.get(v); if ( nRight != null && !nRight.equals(nLeft) ) return false; } diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/algebra/TableFactory.java b/jena-arq/src/main/java/org/apache/jena/sparql/algebra/TableFactory.java index 3f5a5e07e3b..cf630dddb84 100644 --- a/jena-arq/src/main/java/org/apache/jena/sparql/algebra/TableFactory.java +++ b/jena-arq/src/main/java/org/apache/jena/sparql/algebra/TableFactory.java @@ -22,36 +22,55 @@ import org.apache.jena.graph.Node ; import org.apache.jena.sparql.algebra.table.Table1 ; +import org.apache.jena.sparql.algebra.table.TableBuilder; import org.apache.jena.sparql.algebra.table.TableEmpty ; import org.apache.jena.sparql.algebra.table.TableN ; import org.apache.jena.sparql.algebra.table.TableUnit ; import org.apache.jena.sparql.core.Var ; import org.apache.jena.sparql.engine.QueryIterator ; +import org.apache.jena.sparql.engine.binding.Binding ; +import org.apache.jena.sparql.exec.RowSet ; public class TableFactory { public static Table createUnit() { return new TableUnit() ; } - + public static Table createEmpty() { return new TableEmpty() ; } public static Table create() { return new TableN() ; } - + public static Table create(List vars) { return new TableN(vars) ; } - + public static Table create(QueryIterator queryIterator) - { + { if ( queryIterator.isJoinIdentity() ) { queryIterator.close(); return createUnit() ; } - - return new TableN(queryIterator) ; + + return builder().consumeRowsAndVars(queryIterator).build(); } public static Table create(Var var, Node value) { return new Table1(var, value) ; } + + /** Creates a table from the detached bindings of the row set. */ + public static Table create(RowSet rs) + { + TableBuilder builder = builder(); + builder.addVars(rs.getResultVars()); + rs.forEach(row -> { + Binding b = row.detach(); + builder.addRow(b); + }); + return builder.build(); + } + + public static TableBuilder builder() { + return new TableBuilder(); + } } diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/algebra/table/TableBuilder.java b/jena-arq/src/main/java/org/apache/jena/sparql/algebra/table/TableBuilder.java new file mode 100644 index 00000000000..12324010e9d --- /dev/null +++ b/jena-arq/src/main/java/org/apache/jena/sparql/algebra/table/TableBuilder.java @@ -0,0 +1,190 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jena.sparql.algebra.table; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; + +import org.apache.jena.sparql.algebra.Table; +import org.apache.jena.sparql.core.Var; +import org.apache.jena.sparql.engine.QueryIterator; +import org.apache.jena.sparql.engine.binding.Binding; +import org.apache.jena.sparql.engine.join.ImmutableUniqueList; + +/** + * Builder for immutable instances of {@link Table}. + * This builder is not thread safe. + */ +public class TableBuilder { + private ImmutableUniqueList.Builder varsBuilder = ImmutableUniqueList.newUniqueListBuilder(Var.class); + + private List rows = new ArrayList<>(); + private boolean copyRowsOnNextMutation = false; + + // Vars ---- + + /** Returns an immutable snapshot of this builder's current variables. */ + public List snapshotVars() { + return varsBuilder.build(); + } + + public int sizeVars() { + return varsBuilder.size(); + } + + public TableBuilder addVar(Var var) { + varsBuilder.add(var); + return this; + } + + public TableBuilder addVars(Collection vars) { + varsBuilder.addAll(vars); + return this; + } + + public TableBuilder addVars(Iterator vars) { + vars.forEachRemaining(varsBuilder::add); + return this; + } + + /** Adds the variables of a binding but not the binding itself. */ + public TableBuilder addVarsFromRow(Binding row) { + row.vars().forEachRemaining(varsBuilder::add); + return this; + } + + // Rows ----- + + private void copyRowsIfNeeded() { + if (copyRowsOnNextMutation) { + rows = new ArrayList<>(rows); + copyRowsOnNextMutation = false; + } + } + + /** Returns an immutable snapshot of this builder's current rows. */ + public List snapshotRows() { + return List.copyOf(rows); + } + + public int sizeRows() { + return rows.size(); + } + + public TableBuilder addRow(Binding row) { + copyRowsIfNeeded(); + rows.add(row); + return this; + } + + public TableBuilder addRows(Collection newRows) { + copyRowsIfNeeded(); + rows.addAll(newRows); + return this; + } + + public TableBuilder addRows(Iterator newRows) { + copyRowsIfNeeded(); + newRows.forEachRemaining(rows::add); + return this; + } + + // Rows and Vars ----- + + /** This method assumes prior call to copyRowsIfNeeded(). */ + private void addRowAndVarsInternal(Binding row) { + addVarsFromRow(row); + rows.add(row); + } + + public TableBuilder addRowAndVars(Binding row) { + copyRowsIfNeeded(); + addRowAndVarsInternal(row); + return this; + } + + public TableBuilder addRowsAndVars(Collection newRows) { + copyRowsIfNeeded(); + newRows.forEach(this::addVarsFromRow); + rows.addAll(newRows); + return this; + } + + public TableBuilder addRowsAndVars(Iterator newRows) { + copyRowsIfNeeded(); + newRows.forEachRemaining(this::addRowAndVarsInternal); + return this; + } + + /** Add the rows and variables of another table. */ + public TableBuilder addRowsAndVars(Table table) { + addVars(table.getVars()); + addRows(table.rows()); + return this; + } + + /** + * Similar to {@link #addRowsAndVars(Iterator)} but + * also closes the given QueryIterator when done. + */ + public TableBuilder consumeRowsAndVars(QueryIterator qIter) { + Objects.requireNonNull(qIter); + try { + addRowsAndVars(qIter); + } finally { + qIter.close(); + } + return this; + } + + // General ----- + + public TableBuilder resetVars() { + varsBuilder.clear(); + return this; + } + + public TableBuilder resetRows() { + if (copyRowsOnNextMutation) { + rows = new ArrayList<>(); + copyRowsOnNextMutation = false; + } else { + rows.clear(); + } + return this; + } + + /** Reset variables and rows. */ + public TableBuilder reset() { + resetVars(); + resetRows(); + return this; + } + + public Table build() { + List finalVars = snapshotVars(); + List finalRows = Collections.unmodifiableList(rows); + copyRowsOnNextMutation = true; + return new TableData(finalVars, finalRows); + } +} diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/algebra/table/TableData.java b/jena-arq/src/main/java/org/apache/jena/sparql/algebra/table/TableData.java index 99d5fe5c15f..ca7c9f686cb 100644 --- a/jena-arq/src/main/java/org/apache/jena/sparql/algebra/table/TableData.java +++ b/jena-arq/src/main/java/org/apache/jena/sparql/algebra/table/TableData.java @@ -18,23 +18,21 @@ package org.apache.jena.sparql.algebra.table ; +import java.util.Collections; import java.util.List ; import org.apache.jena.sparql.ARQException ; import org.apache.jena.sparql.core.Var ; import org.apache.jena.sparql.engine.binding.Binding ; +/** Immutable table. */ public class TableData extends TableN { public TableData(List variables, List rows) { - super(variables, rows) ; + super(Collections.unmodifiableList(variables), Collections.unmodifiableList(rows)) ; } @Override public void addBinding(Binding binding) { throw new ARQException("Can't add bindings to an existing data table") ; } - - public List getRows() { - return rows ; - } } diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/algebra/table/TableN.java b/jena-arq/src/main/java/org/apache/jena/sparql/algebra/table/TableN.java index 4d8887116c6..03007b2e6db 100644 --- a/jena-arq/src/main/java/org/apache/jena/sparql/algebra/table/TableN.java +++ b/jena-arq/src/main/java/org/apache/jena/sparql/algebra/table/TableN.java @@ -21,6 +21,7 @@ import java.util.ArrayList ; import java.util.Iterator ; import java.util.List ; +import java.util.Objects ; import org.apache.jena.sparql.core.Var ; import org.apache.jena.sparql.engine.ExecutionContext ; @@ -49,15 +50,12 @@ public TableN(QueryIterator qIter) { } protected TableN(List variables, List rows) { - this.vars = variables ; - this.rows = rows ; + this.vars = Objects.requireNonNull(variables) ; + this.rows = Objects.requireNonNull(rows) ; } private void materialize(QueryIterator qIter) { - while (qIter.hasNext()) { - Binding binding = qIter.nextBinding() ; - addBinding(binding) ; - } + qIter.forEachRemaining(this::addBinding); qIter.close() ; } @@ -105,4 +103,8 @@ public List getVarNames() { public List getVars() { return vars ; } + + public List getRows() { + return rows; + } } 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 d7c7efbc5c8..ec59a63cc64 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 @@ -90,4 +90,11 @@ public default boolean contains(String varName) { @Override public boolean equals(Object other); + + /** + * Returns a binding which is guaranteed to be independent of + * any resources such as an ongoing query execution or a disk-based dataset. + * May return itself if it is already detached. + */ + public Binding detach(); } diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/engine/binding/Binding0.java b/jena-arq/src/main/java/org/apache/jena/sparql/engine/binding/Binding0.java index 7833f9e2420..ca06ee4c742 100644 --- a/jena-arq/src/main/java/org/apache/jena/sparql/engine/binding/Binding0.java +++ b/jena-arq/src/main/java/org/apache/jena/sparql/engine/binding/Binding0.java @@ -51,4 +51,9 @@ protected void forEach1(BiConsumer action) { } @Override protected Node get1(Var var) { return null; } + + @Override + protected Binding detachWithNewParent(Binding newParent) { + return new Binding0(newParent); + } } diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/engine/binding/Binding1.java b/jena-arq/src/main/java/org/apache/jena/sparql/engine/binding/Binding1.java index ba21241c0b3..7d8a8703b6a 100644 --- a/jena-arq/src/main/java/org/apache/jena/sparql/engine/binding/Binding1.java +++ b/jena-arq/src/main/java/org/apache/jena/sparql/engine/binding/Binding1.java @@ -70,4 +70,9 @@ protected Node get1(Var v) { return value; return null; } + + @Override + protected Binding detachWithNewParent(Binding newParent) { + return new Binding1(newParent, var, value); + } } diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/engine/binding/Binding2.java b/jena-arq/src/main/java/org/apache/jena/sparql/engine/binding/Binding2.java index 9ad3bc5af0c..42ca53313f4 100644 --- a/jena-arq/src/main/java/org/apache/jena/sparql/engine/binding/Binding2.java +++ b/jena-arq/src/main/java/org/apache/jena/sparql/engine/binding/Binding2.java @@ -77,4 +77,9 @@ protected Node get1(Var v) return value2; return null; } + + @Override + protected Binding detachWithNewParent(Binding newParent) { + return new Binding2(newParent, var1, value1, var2, value2); + } } diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/engine/binding/Binding3.java b/jena-arq/src/main/java/org/apache/jena/sparql/engine/binding/Binding3.java index c52eb07c39b..144cbf40e12 100644 --- a/jena-arq/src/main/java/org/apache/jena/sparql/engine/binding/Binding3.java +++ b/jena-arq/src/main/java/org/apache/jena/sparql/engine/binding/Binding3.java @@ -132,4 +132,9 @@ protected Node get1(Var var) { return null; } + + @Override + protected Binding detachWithNewParent(Binding newParent) { + return new Binding3(newParent, var1, value1, var2, value2, var3, value3); + } } diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/engine/binding/Binding4.java b/jena-arq/src/main/java/org/apache/jena/sparql/engine/binding/Binding4.java index 5ec9e398248..0d71a9ea890 100644 --- a/jena-arq/src/main/java/org/apache/jena/sparql/engine/binding/Binding4.java +++ b/jena-arq/src/main/java/org/apache/jena/sparql/engine/binding/Binding4.java @@ -154,4 +154,9 @@ protected Node get1(Var var) { return null; } + + @Override + protected Binding detachWithNewParent(Binding newParent) { + return new Binding4(newParent, var1, value1, var2, value2, var3, value3, var4, value4); + } } 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 8952425640d..d3544f859c6 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 @@ -202,4 +202,19 @@ public static int hashCode(Binding bind) { } return hash; } + + @Override + public Binding detach() { + Binding newParent = (parent == null) ? null : parent.detach(); + Binding result = (newParent == parent) + ? detachWithOriginalParent() + : detachWithNewParent(newParent); + return result; + } + + protected Binding detachWithOriginalParent() { + return this; + } + + protected abstract Binding detachWithNewParent(Binding newParent); } diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/engine/binding/BindingOverMap.java b/jena-arq/src/main/java/org/apache/jena/sparql/engine/binding/BindingOverMap.java index 04db856512f..555d3f353d8 100644 --- a/jena-arq/src/main/java/org/apache/jena/sparql/engine/binding/BindingOverMap.java +++ b/jena-arq/src/main/java/org/apache/jena/sparql/engine/binding/BindingOverMap.java @@ -61,4 +61,9 @@ protected int size1() { protected boolean isEmpty1() { return map.isEmpty(); } + + @Override + protected Binding detachWithNewParent(Binding newParent) { + return new BindingOverMap(newParent, map); + } } diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/engine/binding/BindingProject.java b/jena-arq/src/main/java/org/apache/jena/sparql/engine/binding/BindingProject.java index ab37542a02c..d84f09d6635 100644 --- a/jena-arq/src/main/java/org/apache/jena/sparql/engine/binding/BindingProject.java +++ b/jena-arq/src/main/java/org/apache/jena/sparql/engine/binding/BindingProject.java @@ -35,4 +35,17 @@ public BindingProject(Collection vars, Binding bind) { protected boolean accept(Var var) { return projectionVars.contains(var) ; } + + @Override + public Binding detach() { + Binding b = binding.detach(); + return b == binding + ? this + : new BindingProject(projectionVars, b); + } + + @Override + protected Binding detachWithNewParent(Binding newParent) { + throw new UnsupportedOperationException("Should never be called."); + } } diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/engine/binding/BindingProjectBase.java b/jena-arq/src/main/java/org/apache/jena/sparql/engine/binding/BindingProjectBase.java index 1364f57d17b..a2156f82fdf 100644 --- a/jena-arq/src/main/java/org/apache/jena/sparql/engine/binding/BindingProjectBase.java +++ b/jena-arq/src/main/java/org/apache/jena/sparql/engine/binding/BindingProjectBase.java @@ -25,13 +25,13 @@ import org.apache.jena.graph.Node ; import org.apache.jena.sparql.core.Var ; -/** Common framework for projection; +/** Common framework for projection; * the projection policy is provided by - * abstract method {@link #accept(Var)} + * abstract method {@link #accept(Var)} */ public abstract class BindingProjectBase extends BindingBase { private List actualVars = null ; - private final Binding binding ; + protected final Binding binding ; public BindingProjectBase(Binding bind) { super(null) ; diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/engine/binding/BindingProjectNamed.java b/jena-arq/src/main/java/org/apache/jena/sparql/engine/binding/BindingProjectNamed.java index aaca87cc1ac..ef956682db2 100644 --- a/jena-arq/src/main/java/org/apache/jena/sparql/engine/binding/BindingProjectNamed.java +++ b/jena-arq/src/main/java/org/apache/jena/sparql/engine/binding/BindingProjectNamed.java @@ -33,4 +33,17 @@ public BindingProjectNamed(Binding bind) { protected boolean accept(Var var) { return var.isNamedVar() ; } + + @Override + public Binding detach() { + Binding b = binding.detach(); + return b == binding + ? this + : new BindingProjectNamed(b); + } + + @Override + protected Binding detachWithNewParent(Binding newParent) { + throw new UnsupportedOperationException("Should never be called."); + } } diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/engine/binding/BindingRoot.java b/jena-arq/src/main/java/org/apache/jena/sparql/engine/binding/BindingRoot.java index 47381b0f7e4..d7743eca12e 100644 --- a/jena-arq/src/main/java/org/apache/jena/sparql/engine/binding/BindingRoot.java +++ b/jena-arq/src/main/java/org/apache/jena/sparql/engine/binding/BindingRoot.java @@ -33,4 +33,9 @@ private BindingRoot() { public void format1(StringBuilder sBuff) { sBuff.append("[Root]"); } + + @Override + protected Binding detachWithNewParent(Binding newParent) { + return this; + } } 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 9ce2ce85a1f..d6944c2c933 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,7 +18,6 @@ package org.apache.jena.sparql.engine.iterator; -import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; @@ -29,10 +28,11 @@ import org.apache.jena.graph.Triple; import org.apache.jena.sparql.algebra.Op; import org.apache.jena.sparql.algebra.Table; +import org.apache.jena.sparql.algebra.TableFactory; import org.apache.jena.sparql.algebra.TransformCopy; import org.apache.jena.sparql.algebra.op.*; import org.apache.jena.sparql.algebra.table.Table1; -import org.apache.jena.sparql.algebra.table.TableN; +import org.apache.jena.sparql.algebra.table.TableBuilder; import org.apache.jena.sparql.core.*; import org.apache.jena.sparql.engine.ExecutionContext; import org.apache.jena.sparql.engine.QueryIterator; @@ -291,18 +291,25 @@ public Op transform(OpTable opTable) { // By the assignment restriction, the binding only needs to be added to each row of the table. Table table = opTable.getTable(); - // Table vars. - List vars = new ArrayList<>(table.getVars()); - binding.vars().forEachRemaining(vars::add); - TableN table2 = new TableN(vars); + + TableBuilder tableBuilder = TableFactory.builder(); + tableBuilder.addVars(table.getVars()); + tableBuilder.addVarsFromRow(binding); + BindingBuilder builder = BindingFactory.builder(); - table.iterator(null).forEachRemaining(row->{ + table.iterator(null).forEachRemaining(row -> { builder.reset(); builder.addAll(row); - builder.addAll(binding); - table2.addBinding(builder.build()); + + // Forcibly add the input binding - this may reassign variables. + // The restriction imposed by SyntaxVarScope.checkLATERAL prevents + // reassignment of a variable to a _different_ value. + binding.forEach(builder::set); + + tableBuilder.addRow(builder.build()); }); - return OpTable.create(table2); + Table newTable = tableBuilder.build(); + return OpTable.create(newTable); } private Triple applyReplacement(Triple triple, Function replacement) { diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/engine/join/ImmutableUniqueList.java b/jena-arq/src/main/java/org/apache/jena/sparql/engine/join/ImmutableUniqueList.java index ed2d1f04291..1ff634e3aea 100644 --- a/jena-arq/src/main/java/org/apache/jena/sparql/engine/join/ImmutableUniqueList.java +++ b/jena-arq/src/main/java/org/apache/jena/sparql/engine/join/ImmutableUniqueList.java @@ -105,6 +105,10 @@ public Builder clear() { return this ; } + public int size() { + return items == null ? 0 : items.size(); + } + public boolean isEmpty() { return items == null || items.isEmpty(); } @@ -183,15 +187,4 @@ public int indexOf(Object o) { } return result; } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - return super.equals(obj); - } } diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/exec/QueryExecBuilder.java b/jena-arq/src/main/java/org/apache/jena/sparql/exec/QueryExecBuilder.java index 43aa09b10fa..ddfba85abc0 100644 --- a/jena-arq/src/main/java/org/apache/jena/sparql/exec/QueryExecBuilder.java +++ b/jena-arq/src/main/java/org/apache/jena/sparql/exec/QueryExecBuilder.java @@ -25,6 +25,9 @@ import org.apache.jena.query.ARQ; import org.apache.jena.query.Query; import org.apache.jena.query.Syntax; +import org.apache.jena.riot.rowset.RowSetOnClose; +import org.apache.jena.sparql.algebra.Table; +import org.apache.jena.sparql.algebra.TableFactory; import org.apache.jena.sparql.core.Var; import org.apache.jena.sparql.engine.binding.Binding; import org.apache.jena.sparql.util.Context; @@ -81,9 +84,16 @@ public default QueryExecBuilder substitution(String var, Node value) { // build-and-use short cuts - /** Build and execute as a SELECT query. */ + /** + * Build and execute as a SELECT query. + * The caller must eventually close the returned RowSet + * in order to free any associated resources. + * Use {@link #table()} to obtain an independent in-memory copy of the row set. + */ public default RowSet select() { - return build().select(); + QueryExec qExec = build(); + RowSet core = qExec.select(); + return new RowSetOnClose(core, qExec::close); } /** Build and execute as a CONSTRUCT query. */ @@ -106,4 +116,18 @@ public default boolean ask() { return qExec.ask(); } } + + /** + * Build and execute as a SELECT query. + * Creates and returns an independent in-memory table by materializing the underlying row set. + * Subsequently, {@link Table#toRowSet()} can be used to obtain a fresh row set view over the table. + */ + public default Table table() { + Table result; + try (QueryExec qExec = build()) { + RowSet rowSet = qExec.select(); + result = TableFactory.create(rowSet); + } + return result; + } } diff --git a/jena-arq/src/test/java/org/apache/jena/query/TestQueryCloningCornerCases.java b/jena-arq/src/test/java/org/apache/jena/query/TestQueryCloningCornerCases.java index 3cc72454afc..b9aa5f90212 100644 --- a/jena-arq/src/test/java/org/apache/jena/query/TestQueryCloningCornerCases.java +++ b/jena-arq/src/test/java/org/apache/jena/query/TestQueryCloningCornerCases.java @@ -17,11 +17,15 @@ */ package org.apache.jena.query; +import java.util.List; + import org.apache.jena.graph.Triple; +import org.apache.jena.sparql.core.Var; import org.apache.jena.sparql.engine.binding.BindingFactory; import org.apache.jena.sparql.syntax.ElementData; import org.apache.jena.sparql.syntax.ElementGroup; import org.apache.jena.sparql.syntax.ElementPathBlock; +import org.apache.jena.sparql.util.NodeFactoryExtra; import org.apache.jena.vocabulary.RDF; import org.junit.Assert; import org.junit.Test; @@ -70,22 +74,17 @@ public void testCloneOfDataAndPathBlocks() public void testCloneOfValuesDataBlock() { String str = "PREFIX eg: " + "SELECT * { ?s eg:foo/eg:bar ?o } VALUES (?s ?o) { (eg:baz 1) }"; - Query query = QueryFactory.create(str); - // Modifications of a query's values data block - // The cloned query's lists of variables and bindings are independent - // from those from the original query - { - Query clone = TestQueryCloningEssentials.checkedClone(query); + // Modifying a clone's value data block must not affect that of the original query. + Query clone = TestQueryCloningEssentials.checkedClone(query); + Assert.assertEquals(query.getValuesData(), clone.getValuesData()); - clone.getValuesData().clear(); - Assert.assertEquals(0, clone.getValuesData().size()); - Assert.assertNotEquals(0, query.getValuesData().size()); + Var x = Var.alloc("x"); + clone.setValuesDataBlock( + List.of(x), + List.of(BindingFactory.binding(x, NodeFactoryExtra.intToNode(1)))); - clone.getValuesVariables().clear(); - Assert.assertEquals(0, clone.getValuesVariables().size()); - Assert.assertNotEquals(0, query.getValuesVariables().size()); - } + Assert.assertNotEquals(query.getValuesData(), clone.getValuesData()); } } diff --git a/jena-arq/src/test/java/org/apache/jena/sparql/algebra/TestTableBuilder.java b/jena-arq/src/test/java/org/apache/jena/sparql/algebra/TestTableBuilder.java new file mode 100644 index 00000000000..b81c35aa74f --- /dev/null +++ b/jena-arq/src/test/java/org/apache/jena/sparql/algebra/TestTableBuilder.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jena.sparql.algebra; + +import org.apache.jena.sparql.algebra.table.TableBuilder; +import org.apache.jena.sparql.core.Var; +import org.apache.jena.sparql.engine.binding.Binding; +import org.apache.jena.sparql.engine.binding.BindingFactory; +import org.apache.jena.sparql.sse.SSE; +import org.apache.jena.sparql.util.NodeFactoryExtra; +import org.junit.Assert; +import org.junit.Test; + +public class TestTableBuilder { + @Test public void table_builder_01() { + Table expectedT1 = SSE.parseTable("(table (vars ?a ?b) (row (?a 1) (?b 2)))"); + Table expectedT2 = SSE.parseTable("(table (vars ?a ?b ?c) (row (?a 1) (?b 2)) (row (?c 3)))"); + + TableBuilder builder = TableFactory.builder(); + Table actualT1 = builder.addRowsAndVars(expectedT1.rows()).build(); + Assert.assertEquals(expectedT1, actualT1); + + // Mutating the builder must not affect the tables created from it. + Binding b = BindingFactory.binding(Var.alloc("c"), NodeFactoryExtra.intToNode(3)); + builder.addRowAndVars(b); + Table actualT2 = builder.build(); + + Assert.assertEquals(expectedT1, actualT1); + Assert.assertEquals(expectedT2, actualT2); + + builder.reset(); + + Assert.assertEquals(expectedT1, actualT1); + Assert.assertEquals(expectedT2, actualT2); + Assert.assertTrue(builder.snapshotVars().isEmpty()); + Assert.assertTrue(builder.snapshotRows().isEmpty()); + } +} + diff --git a/jena-arq/src/test/java/org/apache/jena/sparql/exec/TestQueryExecution.java b/jena-arq/src/test/java/org/apache/jena/sparql/exec/TestQueryExecution.java index 258a3a003fc..7c926a204b7 100644 --- a/jena-arq/src/test/java/org/apache/jena/sparql/exec/TestQueryExecution.java +++ b/jena-arq/src/test/java/org/apache/jena/sparql/exec/TestQueryExecution.java @@ -20,12 +20,10 @@ import static org.junit.jupiter.api.Assertions.assertEquals; -import org.junit.Test; - -import org.apache.jena.graph.Node; -import org.apache.jena.sparql.core.DatasetGraph; +import org.apache.jena.sparql.algebra.Table; import org.apache.jena.sparql.core.DatasetGraphFactory; -import org.apache.jena.sparql.engine.binding.Binding; +import org.apache.jena.sparql.sse.SSE; +import org.junit.Test; /** Miscellaneous tests, e.g. from reports. */ public class TestQueryExecution { @@ -40,11 +38,28 @@ public class TestQueryExecution { } } """; - DatasetGraph dsg = DatasetGraphFactory.empty(); - RowSet rowSet = QueryExec.dataset(dsg).query(qsReport).select(); - Binding row = rowSet.next(); - row.contains("xOut"); - Node x = row.get("xOut"); - assertEquals("x", x.getLiteralLexicalForm()); + + Table expected = SSE.parseTable("(table (row (?xIn 'x') (?x 1) (?xOut 'x') ) )"); + Table actual = QueryExec.dataset(DatasetGraphFactory.empty()).query(qsReport).table(); + assertEquals(expected, actual); + } + + @Test public void lateral_with_nesting() { + // GH-2924 + String qsReport = """ + SELECT * { + BIND(1 AS ?s) + LATERAL { + BIND(?s AS ?x) + LATERAL { + BIND(?s AS ?y) + } + } + } + """; + + Table expected = SSE.parseTable("(table (row (?s 1) (?x 1) (?y 1) ) )"); + Table actual = QueryExec.dataset(DatasetGraphFactory.empty()).query(qsReport).table(); + assertEquals(expected, actual); } } diff --git a/jena-arq/src/test/java/org/apache/jena/sparql/graph/GraphsTests.java b/jena-arq/src/test/java/org/apache/jena/sparql/graph/GraphsTests.java index 90f18dbed13..4e51b3a0f1f 100644 --- a/jena-arq/src/test/java/org/apache/jena/sparql/graph/GraphsTests.java +++ b/jena-arq/src/test/java/org/apache/jena/sparql/graph/GraphsTests.java @@ -28,8 +28,12 @@ import org.apache.jena.query.* ; import org.apache.jena.rdf.model.Model ; import org.apache.jena.rdf.model.ModelFactory ; +import org.apache.jena.sparql.algebra.Table ; import org.apache.jena.sparql.core.Quad ; +import org.apache.jena.sparql.exec.QueryExec ; +import org.apache.jena.sparql.exec.QueryExecBuilder ; import org.apache.jena.sparql.sse.SSE ; +import org.apache.jena.system.Txn ; import org.junit.Test ; /** Test API use of models, including some union graph cases : see also DatasetGraphTests */ @@ -40,12 +44,12 @@ public abstract class GraphsTests protected static final String graph1 = "http://example/g1" ; protected static final String graph2 = "http://example/g2" ; protected static final String graph3 = "http://example/g3" ; - + private Dataset dataset ; private Model calcUnion = ModelFactory.createDefaultModel() ; protected abstract Dataset createDataset() ; - + protected Dataset getDataset() { if ( dataset == null ) @@ -55,17 +59,17 @@ protected Dataset getDataset() } return dataset ; } - + protected void fillDataset(Dataset dataset) { // Load default model. // Load graph 1 // Load graph 2. dataset.getDefaultModel().getGraph().add(SSE.parseTriple("(

'Default graph')")) ; - + Model m1 = dataset.getNamedModel(graph1) ; m1.getGraph().add(SSE.parseTriple("(

'Graph 1')")) ; m1.getGraph().add(SSE.parseTriple("(

'ZZZ')")) ; - + Model m2 = dataset.getNamedModel(graph2) ; m2.getGraph().add(SSE.parseTriple("(

'Graph 2')")) ; m2.getGraph().add(SSE.parseTriple("(

'ZZZ')")) ; @@ -74,30 +78,30 @@ protected void fillDataset(Dataset dataset) { } String queryString = "SELECT * {?s ?p ?o}" ; - - @Test public void graph1() + + @Test public void graph1() { Dataset ds = getDataset() ; int x = query(queryString, ds.getDefaultModel()) ; assertEquals(1,x) ; } - - @Test public void graph2() + + @Test public void graph2() { Dataset ds = getDataset() ; int x = query(queryString, ds.getNamedModel(graph1)) ; assertEquals(2,x) ; } - @Test public void graph3() + @Test public void graph3() { Dataset ds = getDataset() ; int x = query(queryString, ds.getNamedModel(graph3)) ; assertEquals(0,x) ; } - - @Test public void graph4() + + @Test public void graph4() { Dataset ds = getDataset() ; int x = query(queryString, ds.getNamedModel(Quad.unionGraph.getURI())) ; @@ -106,56 +110,80 @@ protected void fillDataset(Dataset dataset) { m.isIsomorphicWith(calcUnion) ; } - @Test public void graph5() + @Test public void graph5() { Dataset ds = getDataset() ; int x = query(queryString, ds.getNamedModel(Quad.defaultGraphIRI.getURI())) ; assertEquals(1,x) ; } - @Test public void graph6() + @Test public void graph6() { Dataset ds = getDataset() ; int x = query(queryString, ds.getNamedModel(Quad.defaultGraphNodeGenerated.getURI())) ; assertEquals(1,x) ; } - @Test public void graph_count1() + /** Test that checks that {@link QueryExecBuilder#table()} correctly detaches the bindings such that they remain + * valid even after the query execution and the data set have been closed. */ + @Test public void table1() + { + // Use a transaction if the reference data set is in one. + Dataset ref = getDataset() ; + + Table expected = SSE.parseTable("(table (row (?s ) (?p

) (?o \"Default graph\") ) )") ; + Table actual ; + Dataset ds = createDataset() ; + try { + if (ref.isInTransaction()) { + Txn.executeWrite(ds, () -> fillDataset(ds)) ; + actual = Txn.calculateRead(ds, () -> QueryExec.dataset(ds.asDatasetGraph()).query(queryString).table()) ; + } else { + fillDataset(ds) ; + actual = QueryExec.dataset(ds.asDatasetGraph()).query(queryString).table() ; + } + } finally { + ds.close() ; + } + assertEquals(expected, actual) ; + } + + @Test public void graph_count1() { Dataset ds = getDataset() ; long x = count(ds.getDefaultModel()) ; assertEquals(1,x) ; } - @Test public void graph_count2() + @Test public void graph_count2() { Dataset ds = getDataset() ; long x = count(ds.getNamedModel(graph1)) ; assertEquals(2,x) ; } - @Test public void graph_count3() + @Test public void graph_count3() { Dataset ds = getDataset() ; long x = count(ds.getNamedModel(graph3)) ; assertEquals(0,x) ; } - - @Test public void graph_count4() + + @Test public void graph_count4() { Dataset ds = getDataset() ; long x = count(ds.getNamedModel(Quad.unionGraph.getURI())) ; assertEquals(3,x) ; } - - @Test public void graph_count5() + + @Test public void graph_count5() { Dataset ds = getDataset() ; long x = count(ds.getNamedModel(Quad.defaultGraphIRI.getURI())) ; assertEquals(1,x) ; } - @Test public void graph_count6() + @Test public void graph_count6() { Dataset ds = getDataset() ; long x = count(ds.getNamedModel(Quad.defaultGraphNodeGenerated.getURI())) ; @@ -170,29 +198,29 @@ protected void fillDataset(Dataset dataset) { assertEquals(0, x) ; } - @Test public void graph_api1() + @Test public void graph_api1() { Dataset ds = getDataset() ; int x = api(ds.getDefaultModel()) ; assertEquals(1,x) ; } - - @Test public void graph_api2() + + @Test public void graph_api2() { Dataset ds = getDataset() ; int x = api(ds.getNamedModel(graph1)) ; assertEquals(2,x) ; } - @Test public void graph_api3() + @Test public void graph_api3() { Dataset ds = getDataset() ; int x = api(ds.getNamedModel(graph3)) ; assertEquals(0,x) ; } - - @Test public void graph_api4() + + @Test public void graph_api4() { Dataset ds = getDataset() ; int x = api(ds.getNamedModel(Quad.unionGraph.getURI())) ; @@ -201,20 +229,20 @@ protected void fillDataset(Dataset dataset) { m.isIsomorphicWith(calcUnion) ; } - @Test public void graph_api5() + @Test public void graph_api5() { Dataset ds = getDataset() ; int x = api(ds.getNamedModel(Quad.defaultGraphIRI.getURI())) ; assertEquals(1,x) ; } - @Test public void graph_api6() + @Test public void graph_api6() { Dataset ds = getDataset() ; int x = api(ds.getNamedModel(Quad.defaultGraphNodeGenerated.getURI())) ; assertEquals(1,x) ; } - + private int query(String str, Model model) { Query q = QueryFactory.create(str, Syntax.syntaxARQ) ; @@ -223,14 +251,14 @@ private int query(String str, Model model) return ResultSetFormatter.consume(rs) ; } } - + private int api(Model model) { Iterator iter = model.getGraph().find(Node.ANY, Node.ANY, Node.ANY) ; int x = (int)Iter.count(iter) ; return x ; } - + private long count(Model model) { return model.size() ; diff --git a/jena-tdb1/src/main/java/org/apache/jena/tdb1/solver/BindingTDB.java b/jena-tdb1/src/main/java/org/apache/jena/tdb1/solver/BindingTDB.java index b07bac36255..8218c99656f 100644 --- a/jena-tdb1/src/main/java/org/apache/jena/tdb1/solver/BindingTDB.java +++ b/jena-tdb1/src/main/java/org/apache/jena/tdb1/solver/BindingTDB.java @@ -26,9 +26,10 @@ import org.apache.jena.sparql.core.Var ; import org.apache.jena.sparql.engine.binding.Binding ; import org.apache.jena.sparql.engine.binding.BindingBase ; -import org.apache.jena.tdb1.TDB1Exception; -import org.apache.jena.tdb1.store.NodeId; -import org.apache.jena.tdb1.store.nodetable.NodeTable; +import org.apache.jena.sparql.engine.binding.BindingFactory ; +import org.apache.jena.tdb1.TDB1Exception ; +import org.apache.jena.tdb1.store.NodeId ; +import org.apache.jena.tdb1.store.nodetable.NodeTable ; /** Bind that delays turning a NodeId into a Node until explicitly needed by get() */ @@ -159,4 +160,14 @@ protected void fmtVar(StringBuilder sbuff, Var var) String tmp = NodeFmtLib.displayStr(node) ; sbuff.append("( ?"+var.getVarName()+extra+" = "+tmp+" )") ; } + + @Override + public Binding detach() { + return BindingFactory.copy(this); + } + + @Override + protected Binding detachWithNewParent(Binding newParent) { + throw new UnsupportedOperationException("Should never be called."); + } } diff --git a/jena-tdb2/src/main/java/org/apache/jena/tdb2/solver/BindingTDB.java b/jena-tdb2/src/main/java/org/apache/jena/tdb2/solver/BindingTDB.java index dc913d1c041..5a77907c857 100644 --- a/jena-tdb2/src/main/java/org/apache/jena/tdb2/solver/BindingTDB.java +++ b/jena-tdb2/src/main/java/org/apache/jena/tdb2/solver/BindingTDB.java @@ -26,6 +26,7 @@ import org.apache.jena.sparql.core.Var; import org.apache.jena.sparql.engine.binding.Binding; import org.apache.jena.sparql.engine.binding.BindingBase; +import org.apache.jena.sparql.engine.binding.BindingFactory; import org.apache.jena.tdb2.TDBException; import org.apache.jena.tdb2.store.NodeId; import org.apache.jena.tdb2.store.nodetable.NodeTable; @@ -159,4 +160,14 @@ protected void fmtVar(StringBuilder sbuff, Var var) String tmp = NodeFmtLib.displayStr(node); sbuff.append("( ?"+var.getVarName()+extra+" = "+tmp+" )"); } + + @Override + public Binding detach() { + return BindingFactory.copy(this); + } + + @Override + protected Binding detachWithNewParent(Binding newParent) { + throw new UnsupportedOperationException("Should never be called."); + } }