diff --git a/core/queryalgebra/evaluation/src/main/java/org/eclipse/rdf4j/query/algebra/evaluation/QueryValueEvaluationStep.java b/core/queryalgebra/evaluation/src/main/java/org/eclipse/rdf4j/query/algebra/evaluation/QueryValueEvaluationStep.java index 33ee5d815d..ded54888b2 100644 --- a/core/queryalgebra/evaluation/src/main/java/org/eclipse/rdf4j/query/algebra/evaluation/QueryValueEvaluationStep.java +++ b/core/queryalgebra/evaluation/src/main/java/org/eclipse/rdf4j/query/algebra/evaluation/QueryValueEvaluationStep.java @@ -12,12 +12,14 @@ package org.eclipse.rdf4j.query.algebra.evaluation; import java.util.function.Function; +import java.util.function.Predicate; import org.eclipse.rdf4j.model.Value; import org.eclipse.rdf4j.query.BindingSet; import org.eclipse.rdf4j.query.QueryEvaluationException; import org.eclipse.rdf4j.query.algebra.ValueConstant; import org.eclipse.rdf4j.query.algebra.ValueExpr; +import org.eclipse.rdf4j.query.algebra.evaluation.util.QueryEvaluationUtility; /** * A step in the query evaluation that works on ValueExpresions. @@ -26,6 +28,18 @@ public interface QueryValueEvaluationStep { Value evaluate(BindingSet bindings) throws QueryEvaluationException; + default Predicate asPredicate() { + return bs -> { + try { + Value value = evaluate(bs); + return QueryEvaluationUtility.getEffectiveBooleanValue(value).orElse(false); + } catch (ValueExprEvaluationException e) { + // Ignore, condition not evaluated successfully + return false; + } + }; + } + /** * If an value expression results in a constant then it may be executed once per query invocation. This can reduce * computation time significantly. diff --git a/core/queryalgebra/evaluation/src/main/java/org/eclipse/rdf4j/query/algebra/evaluation/impl/evaluationsteps/LeftJoinQueryEvaluationStep.java b/core/queryalgebra/evaluation/src/main/java/org/eclipse/rdf4j/query/algebra/evaluation/impl/evaluationsteps/LeftJoinQueryEvaluationStep.java index 7d86ec5cd9..051780a9ee 100644 --- a/core/queryalgebra/evaluation/src/main/java/org/eclipse/rdf4j/query/algebra/evaluation/impl/evaluationsteps/LeftJoinQueryEvaluationStep.java +++ b/core/queryalgebra/evaluation/src/main/java/org/eclipse/rdf4j/query/algebra/evaluation/impl/evaluationsteps/LeftJoinQueryEvaluationStep.java @@ -20,9 +20,8 @@ import org.eclipse.rdf4j.query.algebra.evaluation.QueryEvaluationStep; import org.eclipse.rdf4j.query.algebra.evaluation.QueryValueEvaluationStep; import org.eclipse.rdf4j.query.algebra.evaluation.impl.QueryEvaluationContext; -import org.eclipse.rdf4j.query.algebra.evaluation.iterator.BadlyDesignedLeftJoinIterator; -import org.eclipse.rdf4j.query.algebra.evaluation.iterator.HashJoinIteration; -import org.eclipse.rdf4j.query.algebra.evaluation.iterator.LeftJoinIterator; +import org.eclipse.rdf4j.query.algebra.evaluation.impl.evaluationsteps.values.ScopedQueryValueEvaluationStep; +import org.eclipse.rdf4j.query.algebra.evaluation.iterator.*; import org.eclipse.rdf4j.query.algebra.helpers.TupleExprs; import org.eclipse.rdf4j.query.algebra.helpers.collectors.VarNameCollector; @@ -32,6 +31,7 @@ public final class LeftJoinQueryEvaluationStep implements QueryEvaluationStep { private final QueryEvaluationStep left; private final LeftJoin leftJoin; private final Set optionalVars; + private final QueryEvaluationStep wellDesignedRightEvaluationStep; public static QueryEvaluationStep supply(EvaluationStrategy strategy, LeftJoin leftJoin, QueryEvaluationContext context) { @@ -85,7 +85,11 @@ public LeftJoinQueryEvaluationStep(QueryEvaluationStep right, QueryValueEvaluati } this.optionalVars = optionalVars; - + this.wellDesignedRightEvaluationStep = determineRightEvaluationStep( + leftJoin, + right, + condition, + leftJoin.getBindingNames()); } @Override @@ -103,13 +107,37 @@ public CloseableIteration evaluate(BindingSet bindings) { if (containsNone) { // left join is "well designed" leftJoin.setAlgorithm(LeftJoinIterator.class.getSimpleName()); - return LeftJoinIterator.getInstance(left, right, condition, bindings, leftJoin.getBindingNames()); + return LeftJoinIterator.getInstance(left, bindings, wellDesignedRightEvaluationStep); } else { Set problemVars = new HashSet<>(optionalVars); problemVars.retainAll(bindings.getBindingNames()); leftJoin.setAlgorithm(BadlyDesignedLeftJoinIterator.class.getSimpleName()); - return new BadlyDesignedLeftJoinIterator(left, right, condition, bindings, problemVars); + var rightEvaluationStep = determineRightEvaluationStep(leftJoin, right, condition, problemVars); + return new BadlyDesignedLeftJoinIterator(left, bindings, problemVars, rightEvaluationStep); } } + + public static QueryEvaluationStep determineRightEvaluationStep( + LeftJoin join, + QueryEvaluationStep prepareRightArg, + QueryValueEvaluationStep joinCondition, + Set scopeBindingNames) { + if (joinCondition == null) { + return prepareRightArg; + } else if (canEvaluateConditionBasedOnLeftHandSide(join)) { + return new PreFilterQueryEvaluationStep( + prepareRightArg, + new ScopedQueryValueEvaluationStep(join.getAssuredBindingNames(), joinCondition)); + } else { + return new PostFilterQueryEvaluationStep( + prepareRightArg, + new ScopedQueryValueEvaluationStep(scopeBindingNames, joinCondition)); + } + } + + private static boolean canEvaluateConditionBasedOnLeftHandSide(LeftJoin leftJoin) { + var varNames = VarNameCollector.process(leftJoin.getCondition()); + return leftJoin.getAssuredBindingNames().containsAll(varNames); + } } diff --git a/core/queryalgebra/evaluation/src/main/java/org/eclipse/rdf4j/query/algebra/evaluation/impl/evaluationsteps/PostFilterQueryEvaluationStep.java b/core/queryalgebra/evaluation/src/main/java/org/eclipse/rdf4j/query/algebra/evaluation/impl/evaluationsteps/PostFilterQueryEvaluationStep.java new file mode 100644 index 0000000000..4119f7fa6d --- /dev/null +++ b/core/queryalgebra/evaluation/src/main/java/org/eclipse/rdf4j/query/algebra/evaluation/impl/evaluationsteps/PostFilterQueryEvaluationStep.java @@ -0,0 +1,53 @@ +/******************************************************************************* + * Copyright (c) 2025 Eclipse RDF4J contributors. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Distribution License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + *******************************************************************************/ +package org.eclipse.rdf4j.query.algebra.evaluation.impl.evaluationsteps; + +import java.util.function.Predicate; + +import org.eclipse.rdf4j.common.iteration.CloseableIteration; +import org.eclipse.rdf4j.common.iteration.FilterIteration; +import org.eclipse.rdf4j.query.BindingSet; +import org.eclipse.rdf4j.query.algebra.evaluation.QueryEvaluationStep; +import org.eclipse.rdf4j.query.algebra.evaluation.QueryValueEvaluationStep; + +public class PostFilterQueryEvaluationStep implements QueryEvaluationStep { + + private final QueryEvaluationStep wrapped; + private final Predicate condition; + + public PostFilterQueryEvaluationStep(QueryEvaluationStep wrapped, + QueryValueEvaluationStep condition) { + this.wrapped = wrapped; + this.condition = condition.asPredicate(); + } + + @Override + public CloseableIteration evaluate(BindingSet leftBindings) { + var rightIteration = wrapped.evaluate(leftBindings); + + if (rightIteration == QueryEvaluationStep.EMPTY_ITERATION) { + return rightIteration; + } + + return new FilterIteration<>(rightIteration) { + + @Override + protected boolean accept(BindingSet bindings) { + return condition.test(bindings); + } + + @Override + protected void handleClose() { + // Nothing to close + } + }; + } +} diff --git a/core/queryalgebra/evaluation/src/main/java/org/eclipse/rdf4j/query/algebra/evaluation/impl/evaluationsteps/PreFilterQueryEvaluationStep.java b/core/queryalgebra/evaluation/src/main/java/org/eclipse/rdf4j/query/algebra/evaluation/impl/evaluationsteps/PreFilterQueryEvaluationStep.java new file mode 100644 index 0000000000..9cb90e9608 --- /dev/null +++ b/core/queryalgebra/evaluation/src/main/java/org/eclipse/rdf4j/query/algebra/evaluation/impl/evaluationsteps/PreFilterQueryEvaluationStep.java @@ -0,0 +1,40 @@ +/******************************************************************************* + * Copyright (c) 2025 Eclipse RDF4J contributors. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Distribution License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + *******************************************************************************/ +package org.eclipse.rdf4j.query.algebra.evaluation.impl.evaluationsteps; + +import java.util.function.Predicate; + +import org.eclipse.rdf4j.common.iteration.CloseableIteration; +import org.eclipse.rdf4j.query.BindingSet; +import org.eclipse.rdf4j.query.algebra.evaluation.QueryEvaluationStep; +import org.eclipse.rdf4j.query.algebra.evaluation.QueryValueEvaluationStep; + +public class PreFilterQueryEvaluationStep implements QueryEvaluationStep { + + private final QueryEvaluationStep wrapped; + private final Predicate condition; + + public PreFilterQueryEvaluationStep(QueryEvaluationStep wrapped, + QueryValueEvaluationStep condition) { + this.wrapped = wrapped; + this.condition = condition.asPredicate(); + } + + @Override + public CloseableIteration evaluate(BindingSet leftBindings) { + if (!condition.test(leftBindings)) { + // Usage of this method assume this instance is returned + return QueryEvaluationStep.EMPTY_ITERATION; + } + + return wrapped.evaluate(leftBindings); + } +} diff --git a/core/queryalgebra/evaluation/src/main/java/org/eclipse/rdf4j/query/algebra/evaluation/impl/evaluationsteps/values/ScopedQueryValueEvaluationStep.java b/core/queryalgebra/evaluation/src/main/java/org/eclipse/rdf4j/query/algebra/evaluation/impl/evaluationsteps/values/ScopedQueryValueEvaluationStep.java new file mode 100644 index 0000000000..3845eb8827 --- /dev/null +++ b/core/queryalgebra/evaluation/src/main/java/org/eclipse/rdf4j/query/algebra/evaluation/impl/evaluationsteps/values/ScopedQueryValueEvaluationStep.java @@ -0,0 +1,53 @@ +/******************************************************************************* + * Copyright (c) 2025 Eclipse RDF4J contributors. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Distribution License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + *******************************************************************************/ +package org.eclipse.rdf4j.query.algebra.evaluation.impl.evaluationsteps.values; + +import java.util.Set; + +import org.eclipse.rdf4j.model.Value; +import org.eclipse.rdf4j.query.Binding; +import org.eclipse.rdf4j.query.BindingSet; +import org.eclipse.rdf4j.query.algebra.evaluation.QueryBindingSet; +import org.eclipse.rdf4j.query.algebra.evaluation.QueryValueEvaluationStep; + +public class ScopedQueryValueEvaluationStep implements QueryValueEvaluationStep { + + /** + * The set of binding names that are "in scope" for the filter. The filter must not include bindings that are (only) + * included because of the depth-first evaluation strategy in the evaluation of the constraint. + */ + private final Set scopeBindingNames; + private final QueryValueEvaluationStep wrapped; + + public ScopedQueryValueEvaluationStep(Set scopeBindingNames, QueryValueEvaluationStep condition) { + this.scopeBindingNames = scopeBindingNames; + this.wrapped = condition; + } + + @Override + public Value evaluate(BindingSet bindings) { + BindingSet scopeBindings = createScopeBindings(scopeBindingNames, bindings); + + return wrapped.evaluate(scopeBindings); + } + + private BindingSet createScopeBindings(Set scopeBindingNames, BindingSet bindings) { + QueryBindingSet scopeBindings = new QueryBindingSet(scopeBindingNames.size()); + for (String scopeBindingName : scopeBindingNames) { + Binding binding = bindings.getBinding(scopeBindingName); + if (binding != null) { + scopeBindings.addBinding(binding); + } + } + + return scopeBindings; + } +} diff --git a/core/queryalgebra/evaluation/src/main/java/org/eclipse/rdf4j/query/algebra/evaluation/iterator/BadlyDesignedLeftJoinIterator.java b/core/queryalgebra/evaluation/src/main/java/org/eclipse/rdf4j/query/algebra/evaluation/iterator/BadlyDesignedLeftJoinIterator.java index e24ea11653..a80e93c01b 100644 --- a/core/queryalgebra/evaluation/src/main/java/org/eclipse/rdf4j/query/algebra/evaluation/iterator/BadlyDesignedLeftJoinIterator.java +++ b/core/queryalgebra/evaluation/src/main/java/org/eclipse/rdf4j/query/algebra/evaluation/iterator/BadlyDesignedLeftJoinIterator.java @@ -40,9 +40,13 @@ public class BadlyDesignedLeftJoinIterator extends LeftJoinIterator { * Constructors * *--------------*/ - public BadlyDesignedLeftJoinIterator(EvaluationStrategy strategy, LeftJoin join, BindingSet inputBindings, - Set problemVars, QueryEvaluationContext context) throws QueryEvaluationException { - super(strategy, join, getFilteredBindings(inputBindings, problemVars), context); + public BadlyDesignedLeftJoinIterator( + EvaluationStrategy strategy, + LeftJoin join, + BindingSet inputBindings, + Set problemVars, + QueryEvaluationStep rightEvaluationStep) throws QueryEvaluationException { + super(strategy, join, getFilteredBindings(inputBindings, problemVars), rightEvaluationStep); this.inputBindings = inputBindings; this.problemVars = problemVars; @@ -52,10 +56,12 @@ public BadlyDesignedLeftJoinIterator(EvaluationStrategy strategy, LeftJoin join, * Methods * *---------*/ - public BadlyDesignedLeftJoinIterator(QueryEvaluationStep left, QueryEvaluationStep right, - QueryValueEvaluationStep joinCondition, BindingSet inputBindings, Set problemVars) + public BadlyDesignedLeftJoinIterator(QueryEvaluationStep left, + BindingSet inputBindings, + Set problemVars, + QueryEvaluationStep rightEvaluationStep) throws QueryEvaluationException { - super(left, right, joinCondition, getFilteredBindings(inputBindings, problemVars), problemVars); + super(left, getFilteredBindings(inputBindings, problemVars), rightEvaluationStep); this.inputBindings = inputBindings; this.problemVars = problemVars; } diff --git a/core/queryalgebra/evaluation/src/main/java/org/eclipse/rdf4j/query/algebra/evaluation/iterator/LeftJoinIterator.java b/core/queryalgebra/evaluation/src/main/java/org/eclipse/rdf4j/query/algebra/evaluation/iterator/LeftJoinIterator.java index adae64942a..7fc65bc694 100644 --- a/core/queryalgebra/evaluation/src/main/java/org/eclipse/rdf4j/query/algebra/evaluation/iterator/LeftJoinIterator.java +++ b/core/queryalgebra/evaluation/src/main/java/org/eclipse/rdf4j/query/algebra/evaluation/iterator/LeftJoinIterator.java @@ -11,102 +11,68 @@ package org.eclipse.rdf4j.query.algebra.evaluation.iterator; import java.util.NoSuchElementException; -import java.util.Set; import org.eclipse.rdf4j.common.iteration.CloseableIteration; import org.eclipse.rdf4j.common.iteration.LookAheadIteration; -import org.eclipse.rdf4j.model.Value; -import org.eclipse.rdf4j.query.Binding; import org.eclipse.rdf4j.query.BindingSet; import org.eclipse.rdf4j.query.QueryEvaluationException; import org.eclipse.rdf4j.query.algebra.LeftJoin; -import org.eclipse.rdf4j.query.algebra.ValueExpr; import org.eclipse.rdf4j.query.algebra.evaluation.EvaluationStrategy; -import org.eclipse.rdf4j.query.algebra.evaluation.QueryBindingSet; import org.eclipse.rdf4j.query.algebra.evaluation.QueryEvaluationStep; -import org.eclipse.rdf4j.query.algebra.evaluation.QueryValueEvaluationStep; -import org.eclipse.rdf4j.query.algebra.evaluation.ValueExprEvaluationException; -import org.eclipse.rdf4j.query.algebra.evaluation.impl.QueryEvaluationContext; -import org.eclipse.rdf4j.query.algebra.evaluation.util.QueryEvaluationUtility; public class LeftJoinIterator extends LookAheadIteration { /*-----------* * Variables * *-----------*/ - /** - * The set of binding names that are "in scope" for the filter. The filter must not include bindings that are (only) - * included because of the depth-first evaluation strategy in the evaluation of the constraint. - */ - private final Set scopeBindingNames; - private final CloseableIteration leftIter; + private final QueryEvaluationStep rightEvaluationStep; private CloseableIteration rightIter; - private final QueryEvaluationStep prepareRightArg; - - private final QueryValueEvaluationStep joinCondition; - /*--------------* * Constructors * *--------------*/ - public LeftJoinIterator(EvaluationStrategy strategy, LeftJoin join, BindingSet bindings, - QueryEvaluationContext context) - throws QueryEvaluationException { - this.scopeBindingNames = join.getBindingNames(); - + public LeftJoinIterator( + EvaluationStrategy strategy, + LeftJoin join, + BindingSet bindings, + QueryEvaluationStep rightEvaluationStep) throws QueryEvaluationException { leftIter = strategy.evaluate(join.getLeftArg(), bindings); rightIter = null; - prepareRightArg = strategy.precompile(join.getRightArg(), context); join.setAlgorithm(this); - final ValueExpr condition = join.getCondition(); - if (condition == null) { - joinCondition = null; - } else { - joinCondition = strategy.precompile(condition, context); - } - } - - public LeftJoinIterator(QueryEvaluationStep left, QueryEvaluationStep right, QueryValueEvaluationStep joinCondition, - BindingSet bindings, Set scopeBindingNamse) - throws QueryEvaluationException { - this.scopeBindingNames = scopeBindingNamse; - - leftIter = left.evaluate(bindings); - // Initialize with empty iteration so that var is never null - rightIter = null; - - prepareRightArg = right; - this.joinCondition = joinCondition; + this.rightEvaluationStep = rightEvaluationStep; + } + public LeftJoinIterator( + QueryEvaluationStep left, + BindingSet bindings, + QueryEvaluationStep rightEvaluationStep) throws QueryEvaluationException { + this(left.evaluate(bindings), rightEvaluationStep); } - public LeftJoinIterator(CloseableIteration leftIter, QueryEvaluationStep prepareRightArg, - QueryValueEvaluationStep joinCondition, Set scopeBindingNamse) { - this.scopeBindingNames = scopeBindingNamse; + public LeftJoinIterator(CloseableIteration leftIter, QueryEvaluationStep rightEvaluationStep) { this.leftIter = leftIter; this.rightIter = null; - this.prepareRightArg = prepareRightArg; - this.joinCondition = joinCondition; + this.rightEvaluationStep = rightEvaluationStep; } - public static CloseableIteration getInstance(QueryEvaluationStep left, - QueryEvaluationStep prepareRightArg, QueryValueEvaluationStep joinCondition, BindingSet bindings, - Set scopeBindingNamse) { + public static CloseableIteration getInstance( + QueryEvaluationStep left, + BindingSet bindings, + QueryEvaluationStep rightEvaluationStep) { CloseableIteration leftIter = left.evaluate(bindings); if (leftIter == QueryEvaluationStep.EMPTY_ITERATION) { return leftIter; } else { - return new LeftJoinIterator(leftIter, prepareRightArg, joinCondition, scopeBindingNamse); + return new LeftJoinIterator(leftIter, rightEvaluationStep); } - } /*---------* @@ -125,17 +91,16 @@ protected BindingSet getNextElement() throws QueryEvaluationException { if (leftIter.hasNext()) { // Use left arg's bindings in case join fails leftBindings = leftIter.next(); - nextRightIter = rightIter = prepareRightArg.evaluate(leftBindings); + nextRightIter = rightIter = rightEvaluationStep.evaluate(leftBindings); } else { return null; } - } else if (!nextRightIter.hasNext()) { // Use left arg's bindings in case join fails leftBindings = leftIter.next(); nextRightIter.close(); - nextRightIter = rightIter = prepareRightArg.evaluate(leftBindings); + nextRightIter = rightIter = rightEvaluationStep.evaluate(leftBindings); } if (nextRightIter == QueryEvaluationStep.EMPTY_ITERATION) { @@ -143,31 +108,8 @@ protected BindingSet getNextElement() throws QueryEvaluationException { return leftBindings; } - while (nextRightIter.hasNext()) { - BindingSet rightBindings = nextRightIter.next(); - - try { - if (joinCondition == null) { - return rightBindings; - } else { - // Limit the bindings to the ones that are in scope for - // this filter - - QueryBindingSet scopeBindings = new QueryBindingSet(scopeBindingNames.size()); - for (String scopeBindingName : scopeBindingNames) { - Binding binding = rightBindings.getBinding(scopeBindingName); - if (binding != null) { - scopeBindings.addBinding(binding); - } - } - - if (isTrue(joinCondition, scopeBindings)) { - return rightBindings; - } - } - } catch (ValueExprEvaluationException e) { - // Ignore, condition not evaluated successfully - } + if (nextRightIter.hasNext()) { + return nextRightIter.next(); } if (leftBindings != null) { @@ -184,11 +126,6 @@ protected BindingSet getNextElement() throws QueryEvaluationException { return null; } - private boolean isTrue(QueryValueEvaluationStep expr, QueryBindingSet bindings) { - Value value = expr.evaluate(bindings); - return QueryEvaluationUtility.getEffectiveBooleanValue(value).orElse(false); - } - @Override protected void handleClose() throws QueryEvaluationException { try { diff --git a/core/queryalgebra/evaluation/src/test/java/org/eclipse/rdf4j/query/algebra/evaluation/impl/evaluationsteps/PostFilterQueryEvaluationStepTest.java b/core/queryalgebra/evaluation/src/test/java/org/eclipse/rdf4j/query/algebra/evaluation/impl/evaluationsteps/PostFilterQueryEvaluationStepTest.java new file mode 100644 index 0000000000..1b006b06e4 --- /dev/null +++ b/core/queryalgebra/evaluation/src/test/java/org/eclipse/rdf4j/query/algebra/evaluation/impl/evaluationsteps/PostFilterQueryEvaluationStepTest.java @@ -0,0 +1,107 @@ +/******************************************************************************* + * Copyright (c) 2025 Eclipse RDF4J contributors. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Distribution License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + *******************************************************************************/ +package org.eclipse.rdf4j.query.algebra.evaluation.impl.evaluationsteps; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.LinkedList; +import java.util.Queue; + +import org.eclipse.rdf4j.common.iteration.CloseableIteration; +import org.eclipse.rdf4j.model.ValueFactory; +import org.eclipse.rdf4j.model.impl.BooleanLiteral; +import org.eclipse.rdf4j.model.impl.SimpleValueFactory; +import org.eclipse.rdf4j.query.BindingSet; +import org.eclipse.rdf4j.query.algebra.evaluation.QueryBindingSet; +import org.eclipse.rdf4j.query.algebra.evaluation.QueryEvaluationStep; +import org.eclipse.rdf4j.query.algebra.evaluation.QueryValueEvaluationStep; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class PostFilterQueryEvaluationStepTest { + + private static final ValueFactory VALUE_FACTORY = SimpleValueFactory.getInstance(); + + private QueryBindingSet bindingSet; + + @BeforeEach + void setUp() { + bindingSet = new QueryBindingSet(1); + bindingSet.addBinding("a", VALUE_FACTORY.createLiteral(42)); + } + + @Test + @DisplayName("when wrapped returns empty iteration, then LeftJoinPostFilterQueryEvaluationStep returns empty iteration") + void emptyIteration() { + QueryEvaluationStep wrapped = (bindings) -> QueryEvaluationStep.EMPTY_ITERATION; + QueryEvaluationStep postFilter = new PostFilterQueryEvaluationStep( + wrapped, + bindings -> BooleanLiteral.valueOf(true)); + + var result = postFilter.evaluate(bindingSet); + + assertThat(result).isEqualTo(QueryEvaluationStep.EMPTY_ITERATION); + } + + @Test + @DisplayName("when wrapped returns non-empty iteration, then LeftJoinPostFilterQueryEvaluationStep returns filtered iteration") + void filteredIteration() { + QueryEvaluationStep wrapped = new TestQueryEvaluationStep(); + QueryValueEvaluationStep condition = bindings -> { + var shouldAccept = bindings.getValue("b") + .stringValue() + .equals("abc"); + return BooleanLiteral.valueOf(shouldAccept); + }; + QueryEvaluationStep postFilter = new PostFilterQueryEvaluationStep(wrapped, condition); + + var result = postFilter.evaluate(bindingSet); + + assertThat(result) + .toIterable() + .map(bindings -> bindings.getValue("b")) + .containsExactly(VALUE_FACTORY.createLiteral("abc")); + } + + private static class TestQueryEvaluationStep implements QueryEvaluationStep { + + private final Queue values = new LinkedList<>(); + + private TestQueryEvaluationStep() { + values.add("abc"); + values.add("xyz"); + } + + @Override + public CloseableIteration evaluate(BindingSet bindings) { + return new CloseableIteration<>() { + @Override + public void close() { + // Nothing to close + } + + @Override + public boolean hasNext() { + return !values.isEmpty(); + } + + @Override + public BindingSet next() { + var output = new QueryBindingSet(2); + bindings.forEach(output::addBinding); + output.addBinding("b", VALUE_FACTORY.createLiteral(values.poll())); + return output; + } + }; + } + } +} diff --git a/core/queryalgebra/evaluation/src/test/java/org/eclipse/rdf4j/query/algebra/evaluation/impl/evaluationsteps/PreFilterQueryEvaluationStepTest.java b/core/queryalgebra/evaluation/src/test/java/org/eclipse/rdf4j/query/algebra/evaluation/impl/evaluationsteps/PreFilterQueryEvaluationStepTest.java new file mode 100644 index 0000000000..c03721ee67 --- /dev/null +++ b/core/queryalgebra/evaluation/src/test/java/org/eclipse/rdf4j/query/algebra/evaluation/impl/evaluationsteps/PreFilterQueryEvaluationStepTest.java @@ -0,0 +1,135 @@ +/******************************************************************************* + * Copyright (c) 2025 Eclipse RDF4J contributors. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Distribution License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + *******************************************************************************/ +package org.eclipse.rdf4j.query.algebra.evaluation.impl.evaluationsteps; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.LinkedList; +import java.util.Queue; +import java.util.concurrent.atomic.AtomicInteger; + +import org.eclipse.rdf4j.common.iteration.CloseableIteration; +import org.eclipse.rdf4j.model.ValueFactory; +import org.eclipse.rdf4j.model.impl.BooleanLiteral; +import org.eclipse.rdf4j.model.impl.SimpleValueFactory; +import org.eclipse.rdf4j.query.BindingSet; +import org.eclipse.rdf4j.query.algebra.evaluation.QueryBindingSet; +import org.eclipse.rdf4j.query.algebra.evaluation.QueryEvaluationStep; +import org.eclipse.rdf4j.query.algebra.evaluation.QueryValueEvaluationStep; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class PreFilterQueryEvaluationStepTest { + + private static final ValueFactory VALUE_FACTORY = SimpleValueFactory.getInstance(); + + private QueryBindingSet bindingSet; + + @BeforeEach + void setUp() { + bindingSet = new QueryBindingSet(1); + bindingSet.addBinding("a", VALUE_FACTORY.createLiteral(42)); + } + + @Test + @DisplayName("when condition evaluates to false, then don't evaluate wrapped") + void skipWrappedEvaluation() { + var wrapped = new TestQueryEvaluationStep(); + QueryEvaluationStep preFilter = new PreFilterQueryEvaluationStep( + wrapped, + bindings -> BooleanLiteral.valueOf(false)); + + try (var ignored = preFilter.evaluate(bindingSet)) { + assertThat(wrapped.isEvaluated).isFalse(); + } + } + + @Test + @DisplayName("when condition evaluates to false, then return empty iteration") + void returnOnlyInput() { + var wrapped = new TestQueryEvaluationStep(); + QueryEvaluationStep preFilter = new PreFilterQueryEvaluationStep( + wrapped, + bindings -> BooleanLiteral.valueOf(false)); + + var result = preFilter.evaluate(bindingSet); + + assertThat(result).isEqualTo(QueryEvaluationStep.EMPTY_ITERATION); + } + + @Test + @DisplayName("when condition evaluates to true, then evaluate and return wrapped output") + void evaluateWrapped() { + var wrapped = new TestQueryEvaluationStep(); + QueryEvaluationStep preFilter = new PreFilterQueryEvaluationStep( + wrapped, + bindings1 -> BooleanLiteral.valueOf(true)); + + var result = preFilter.evaluate(bindingSet); + + assertThat(result) + .toIterable() + .map(bindings -> bindings.getValue("b")) + .containsExactly(VALUE_FACTORY.createLiteral("abc"), VALUE_FACTORY.createLiteral("xyz")); + } + + @Test + @DisplayName("when condition evaluates to true, then only evaluate condition once") + void onlyEvaluatesConditionOnce() { + var evaluations = new AtomicInteger(0); + QueryValueEvaluationStep condition = bindings -> { + evaluations.incrementAndGet(); + return BooleanLiteral.valueOf(true); + }; + TestQueryEvaluationStep wrapped = new TestQueryEvaluationStep(); + QueryEvaluationStep preFilter = new PreFilterQueryEvaluationStep(wrapped, condition); + + try (var ignored = preFilter.evaluate(bindingSet)) { + assertThat(evaluations).hasValue(1); + } + } + + private static class TestQueryEvaluationStep implements QueryEvaluationStep { + + private final Queue values = new LinkedList<>(); + private boolean isEvaluated = false; + + private TestQueryEvaluationStep() { + values.add("abc"); + values.add("xyz"); + } + + @Override + public CloseableIteration evaluate(BindingSet bindings) { + isEvaluated = true; + return new CloseableIteration<>() { + @Override + public void close() { + // Nothing to close + } + + @Override + public boolean hasNext() { + return !values.isEmpty(); + } + + @Override + public BindingSet next() { + var output = new QueryBindingSet(2); + bindings.forEach(output::addBinding); + output.addBinding("b", VALUE_FACTORY.createLiteral(values.poll())); + return output; + } + }; + } + } +} diff --git a/core/queryalgebra/evaluation/src/test/java/org/eclipse/rdf4j/query/algebra/evaluation/iterator/LeftJoinIteratorTest.java b/core/queryalgebra/evaluation/src/test/java/org/eclipse/rdf4j/query/algebra/evaluation/iterator/LeftJoinIteratorTest.java new file mode 100644 index 0000000000..e74fa5efe5 --- /dev/null +++ b/core/queryalgebra/evaluation/src/test/java/org/eclipse/rdf4j/query/algebra/evaluation/iterator/LeftJoinIteratorTest.java @@ -0,0 +1,108 @@ +/******************************************************************************* + * Copyright (c) 2025 Eclipse RDF4J contributors. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Distribution License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + *******************************************************************************/ +package org.eclipse.rdf4j.query.algebra.evaluation.iterator; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import org.eclipse.rdf4j.common.iteration.CloseableIteration; +import org.eclipse.rdf4j.model.*; +import org.eclipse.rdf4j.model.impl.SimpleValueFactory; +import org.eclipse.rdf4j.query.BindingSet; +import org.eclipse.rdf4j.query.QueryEvaluationException; +import org.eclipse.rdf4j.query.algebra.*; +import org.eclipse.rdf4j.query.algebra.evaluation.*; +import org.eclipse.rdf4j.query.algebra.evaluation.impl.DefaultEvaluationStrategy; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class LeftJoinIteratorTest { + + private static final QueryBindingSet RIGHT_BINDINGS = new QueryBindingSet(1); + private static final ValueFactory VALUE_FACTORY = SimpleValueFactory.getInstance(); + private static final EvaluationStrategy EVALUATOR = new DefaultEvaluationStrategy(new TripleSource() { + + @Override + public ValueFactory getValueFactory() { + return SimpleValueFactory.getInstance(); + } + + @Override + public CloseableIteration getStatements(Resource subj, IRI pred, + Value obj, Resource... contexts) throws QueryEvaluationException { + return null; + } + }, null); + + private QueryBindingSet bindingSet; + private BindingSetAssignment left; + private QueryEvaluationStep leftHandSide; + private RightHandSideQueryEvaluationStep rightHandSide; + + @BeforeAll + static void beforeAll() { + RIGHT_BINDINGS.addBinding("right", VALUE_FACTORY.createLiteral(42)); + } + + @BeforeEach + void setUp() { + bindingSet = new QueryBindingSet(1); + bindingSet.addBinding("left", VALUE_FACTORY.createLiteral(42)); + + left = new BindingSetAssignment(); + left.setBindingSets(List.of(bindingSet)); + + leftHandSide = EVALUATOR.precompile(left); + rightHandSide = new RightHandSideQueryEvaluationStep(); + } + + @Test + void evaluatesRightHandSideQueryEvaluationStep() { + try (LeftJoinIterator iterator = new LeftJoinIterator(leftHandSide.evaluate(bindingSet), rightHandSide)) { + iterator.getNextElement(); + } + + assertTrue(rightHandSide.isEvaluated); + } + + private static class RightHandSideQueryEvaluationStep implements QueryEvaluationStep { + + private boolean isEvaluated = false; + + @Override + public CloseableIteration evaluate(BindingSet bindings) { + isEvaluated = true; + var bothBindings = new QueryBindingSet(2); + bindings.forEach(bothBindings::addBinding); + RIGHT_BINDINGS.forEach(bothBindings::addBinding); + var rightIterator = List.of(bothBindings).iterator(); + + return new CloseableIteration<>() { + @Override + public void close() { + // Nothing to close + } + + @Override + public boolean hasNext() { + return rightIterator.hasNext(); + } + + @Override + public BindingSet next() { + return rightIterator.next(); + } + }; + } + } +} diff --git a/core/sail/memory/src/test/java/org/eclipse/rdf4j/sail/memory/benchmark/QueryBenchmark.java b/core/sail/memory/src/test/java/org/eclipse/rdf4j/sail/memory/benchmark/QueryBenchmark.java index cad7c7c460..4e4bb21e36 100644 --- a/core/sail/memory/src/test/java/org/eclipse/rdf4j/sail/memory/benchmark/QueryBenchmark.java +++ b/core/sail/memory/src/test/java/org/eclipse/rdf4j/sail/memory/benchmark/QueryBenchmark.java @@ -21,7 +21,6 @@ import org.eclipse.rdf4j.common.transaction.IsolationLevels; import org.eclipse.rdf4j.query.BindingSet; import org.eclipse.rdf4j.query.TupleQueryResult; -import org.eclipse.rdf4j.query.explanation.Explanation; import org.eclipse.rdf4j.repository.sail.SailRepository; import org.eclipse.rdf4j.repository.sail.SailRepositoryConnection; import org.eclipse.rdf4j.rio.RDFFormat; @@ -61,6 +60,8 @@ public class QueryBenchmark { private static final String common_themes; private static final String different_datasets_with_similar_distributions; private static final String long_chain; + private static final String optional_lhs_filter; + private static final String optional_rhs_filter; private static final String lots_of_optional; private static final String minus; private static final String nested_optionals; @@ -79,6 +80,10 @@ public class QueryBenchmark { getResourceAsStream("benchmarkFiles/different-datasets-with-similar-distributions.qr"), StandardCharsets.UTF_8); long_chain = IOUtils.toString(getResourceAsStream("benchmarkFiles/long-chain.qr"), StandardCharsets.UTF_8); + optional_lhs_filter = IOUtils.toString(getResourceAsStream("benchmarkFiles/optional-lhs-filter.qr"), + StandardCharsets.UTF_8); + optional_rhs_filter = IOUtils.toString(getResourceAsStream("benchmarkFiles/optional-rhs-filter.qr"), + StandardCharsets.UTF_8); lots_of_optional = IOUtils.toString(getResourceAsStream("benchmarkFiles/lots-of-optional.qr"), StandardCharsets.UTF_8); minus = IOUtils.toString(getResourceAsStream("benchmarkFiles/minus.qr"), StandardCharsets.UTF_8); @@ -303,6 +308,24 @@ public long long_chain() { } } + @Benchmark + public long optional_lhs_filter() { + try (SailRepositoryConnection connection = repository.getConnection()) { + return count(connection + .prepareTupleQuery(optional_lhs_filter) + .evaluate()); + } + } + + @Benchmark + public long optional_rhs_filter() { + try (SailRepositoryConnection connection = repository.getConnection()) { + return count(connection + .prepareTupleQuery(optional_rhs_filter) + .evaluate()); + } + } + @Benchmark public long lots_of_optional() { try (SailRepositoryConnection connection = repository.getConnection()) { diff --git a/core/sail/memory/src/test/resources/benchmarkFiles/optional-lhs-filter.qr b/core/sail/memory/src/test/resources/benchmarkFiles/optional-lhs-filter.qr new file mode 100644 index 0000000000..e31be4318c --- /dev/null +++ b/core/sail/memory/src/test/resources/benchmarkFiles/optional-lhs-filter.qr @@ -0,0 +1,25 @@ +PREFIX ex: +PREFIX owl: +PREFIX rdf: +PREFIX rdfs: +PREFIX sh: +PREFIX xsd: +PREFIX dcat: +PREFIX dc: +PREFIX skos: +PREFIX foaf: +PREFIX dct: + +SELECT * WHERE { + + ?dist a dcat:Distribution. + + ?dist dc:license ?license . + + OPTIONAL { + + ?a dcat:distribution ?dist. + + FILTER(?license = ) + } +} diff --git a/core/sail/memory/src/test/resources/benchmarkFiles/optional-rhs-filter.qr b/core/sail/memory/src/test/resources/benchmarkFiles/optional-rhs-filter.qr new file mode 100644 index 0000000000..fc071c4e92 --- /dev/null +++ b/core/sail/memory/src/test/resources/benchmarkFiles/optional-rhs-filter.qr @@ -0,0 +1,26 @@ +PREFIX eu-lang: +PREFIX ex: +PREFIX owl: +PREFIX rdf: +PREFIX rdfs: +PREFIX sh: +PREFIX xsd: +PREFIX dcat: +PREFIX dc: +PREFIX skos: +PREFIX foaf: +PREFIX dct: + +SELECT * WHERE { + + ?dist a dcat:Distribution. + + OPTIONAL { + + ?a dcat:distribution ?dist. + + ?a dct:language $lang. + + FILTER(?lang = eu-lang:ENG) + } +} diff --git a/tools/federation/src/main/java/org/eclipse/rdf4j/federated/evaluation/FederationEvalStrategy.java b/tools/federation/src/main/java/org/eclipse/rdf4j/federated/evaluation/FederationEvalStrategy.java index 25bede851c..04ca4cdca5 100644 --- a/tools/federation/src/main/java/org/eclipse/rdf4j/federated/evaluation/FederationEvalStrategy.java +++ b/tools/federation/src/main/java/org/eclipse/rdf4j/federated/evaluation/FederationEvalStrategy.java @@ -12,6 +12,7 @@ import java.util.HashSet; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicBoolean; @@ -116,6 +117,7 @@ import org.eclipse.rdf4j.query.algebra.evaluation.impl.EvaluationStatistics; import org.eclipse.rdf4j.query.algebra.evaluation.impl.QueryEvaluationContext; import org.eclipse.rdf4j.query.algebra.evaluation.impl.StrictEvaluationStrategy; +import org.eclipse.rdf4j.query.algebra.evaluation.impl.evaluationsteps.LeftJoinQueryEvaluationStep; import org.eclipse.rdf4j.query.algebra.evaluation.iterator.BadlyDesignedLeftJoinIterator; import org.eclipse.rdf4j.query.algebra.evaluation.iterator.HashJoinIteration; import org.eclipse.rdf4j.query.algebra.evaluation.optimizer.ConstantOptimizer; @@ -813,8 +815,21 @@ public CloseableIteration evaluate(BindingSet bindings) { } else { Set problemVarsClone = new HashSet<>(problemVars); problemVarsClone.retainAll(bindings.getBindingNames()); - return new BadlyDesignedLeftJoinIterator(FederationEvalStrategy.this, leftJoin, bindings, - problemVarsClone, context); + QueryEvaluationStep preCompiledRight = this.precompile(leftJoin.getRightArg(), context); + var joinCondition = Optional.ofNullable(leftJoin.getCondition()) + .map(condition -> this.precompile(condition, context)) + .orElse(null); + var rightEvaluationStep = LeftJoinQueryEvaluationStep.determineRightEvaluationStep( + leftJoin, + preCompiledRight, + joinCondition, + problemVars); + return new BadlyDesignedLeftJoinIterator( + FederationEvalStrategy.this, + leftJoin, + bindings, + problemVarsClone, + rightEvaluationStep); } }; }