diff --git a/tools/federation/src/main/java/org/eclipse/rdf4j/federated/algebra/FedXArbitraryLengthPath.java b/tools/federation/src/main/java/org/eclipse/rdf4j/federated/algebra/FedXArbitraryLengthPath.java new file mode 100644 index 00000000000..75ae8bef2bc --- /dev/null +++ b/tools/federation/src/main/java/org/eclipse/rdf4j/federated/algebra/FedXArbitraryLengthPath.java @@ -0,0 +1,54 @@ +/******************************************************************************* + * Copyright (c) 2024 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.federated.algebra; + +import java.util.List; + +import org.eclipse.rdf4j.federated.structures.QueryInfo; +import org.eclipse.rdf4j.federated.util.QueryAlgebraUtil; +import org.eclipse.rdf4j.query.algebra.ArbitraryLengthPath; +import org.eclipse.rdf4j.query.algebra.Var; + +/** + * A specialization of {@link ArbitraryLengthPath} to maintain the {@link QueryInfo} + * + * @author Andreas Schwarte + */ +public class FedXArbitraryLengthPath extends ArbitraryLengthPath implements FedXTupleExpr { + + private static final long serialVersionUID = -7512248084095130084L; + + private final QueryInfo queryInfo; + + public FedXArbitraryLengthPath(ArbitraryLengthPath path, QueryInfo queryInfo) { + super(path.getScope(), clone(path.getSubjectVar()), path.getPathExpression(), clone(path.getObjectVar()), + clone(path.getContextVar()), path.getMinLength()); + this.queryInfo = queryInfo; + } + + private static Var clone(Var var) { + if (var == null) { + return null; + } + return var.clone(); + } + + @Override + public List getFreeVars() { + return List.copyOf(QueryAlgebraUtil.getFreeVars(getPathExpression())); + } + + @Override + public QueryInfo getQueryInfo() { + return queryInfo; + } + +} diff --git a/tools/federation/src/main/java/org/eclipse/rdf4j/federated/algebra/FedXZeroLengthPath.java b/tools/federation/src/main/java/org/eclipse/rdf4j/federated/algebra/FedXZeroLengthPath.java new file mode 100644 index 00000000000..25ef1ea33dc --- /dev/null +++ b/tools/federation/src/main/java/org/eclipse/rdf4j/federated/algebra/FedXZeroLengthPath.java @@ -0,0 +1,49 @@ +/******************************************************************************* + * Copyright (c) 2024 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.federated.algebra; + +import java.util.List; + +import org.eclipse.rdf4j.federated.structures.QueryInfo; +import org.eclipse.rdf4j.query.algebra.StatementPattern.Scope; +import org.eclipse.rdf4j.query.algebra.Var; +import org.eclipse.rdf4j.query.algebra.ZeroLengthPath; + +/** + * A specialization of {@link ZeroLengthPath} that keeps track of {@link QueryInfo} and statement sources. + * + * @author Andreas Schwarte + */ +public class FedXZeroLengthPath extends ZeroLengthPath implements QueryRef { + + private static final long serialVersionUID = 2241037911187178861L; + + private final QueryInfo queryInfo; + + private final List statementSources; + + public FedXZeroLengthPath(Scope scope, Var subjVar, Var objVar, Var conVar, QueryInfo queryInfo, + List statementSources) { + super(scope, subjVar, objVar, conVar); + this.queryInfo = queryInfo; + this.statementSources = statementSources; + } + + public List getStatementSources() { + return statementSources; + } + + @Override + public QueryInfo getQueryInfo() { + return queryInfo; + } + +} diff --git a/tools/federation/src/main/java/org/eclipse/rdf4j/federated/algebra/HolderNode.java b/tools/federation/src/main/java/org/eclipse/rdf4j/federated/algebra/HolderNode.java new file mode 100644 index 00000000000..77e98c50705 --- /dev/null +++ b/tools/federation/src/main/java/org/eclipse/rdf4j/federated/algebra/HolderNode.java @@ -0,0 +1,52 @@ +/******************************************************************************* + * Copyright (c) 2024 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.federated.algebra; + +import org.eclipse.rdf4j.query.algebra.AbstractQueryModelNode; +import org.eclipse.rdf4j.query.algebra.QueryModelNode; +import org.eclipse.rdf4j.query.algebra.QueryModelVisitor; + +/** + * An artificial holder node serving as parent holder to allow replacement. + */ +public class HolderNode extends AbstractQueryModelNode { + + private static final long serialVersionUID = -4689963986499825771L; + private QueryModelNode child; + + public HolderNode(QueryModelNode child) { + super(); + setChild(child); + } + + public void setChild(QueryModelNode child) { + this.child = child; + this.child.setParentNode(this); + } + + @Override + public void visitChildren(QueryModelVisitor visitor) throws X { + child.visit(visitor); + } + + @Override + public void visit(QueryModelVisitor visitor) throws X { + visitor.meetOther(this); + } + + @Override + public void replaceChildNode(QueryModelNode current, QueryModelNode replacement) { + if (child.equals(current)) { + setChild(replacement); + + } + } +} diff --git a/tools/federation/src/main/java/org/eclipse/rdf4j/federated/evaluation/FedXZeroLengthPathEvaluationStep.java b/tools/federation/src/main/java/org/eclipse/rdf4j/federated/evaluation/FedXZeroLengthPathEvaluationStep.java new file mode 100644 index 00000000000..07be87a7824 --- /dev/null +++ b/tools/federation/src/main/java/org/eclipse/rdf4j/federated/evaluation/FedXZeroLengthPathEvaluationStep.java @@ -0,0 +1,91 @@ +/******************************************************************************* + * Copyright (c) 2024 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.federated.evaluation; + +import java.util.List; +import java.util.function.Supplier; + +import org.eclipse.rdf4j.common.iteration.CloseableIteration; +import org.eclipse.rdf4j.federated.algebra.FedXZeroLengthPath; +import org.eclipse.rdf4j.federated.algebra.StatementSource; +import org.eclipse.rdf4j.federated.evaluation.iterator.FedXZeroLengthPathIteration; +import org.eclipse.rdf4j.federated.structures.QueryInfo; +import org.eclipse.rdf4j.model.Value; +import org.eclipse.rdf4j.query.BindingSet; +import org.eclipse.rdf4j.query.QueryEvaluationException; +import org.eclipse.rdf4j.query.algebra.Var; +import org.eclipse.rdf4j.query.algebra.evaluation.EvaluationStrategy; +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.impl.evaluationsteps.ZeroLengthPathEvaluationStep; + +/** + * An evaluation step used for {@link FedXZeroLengthPath}. + * + * @see ZeroLengthPathEvaluationStep + */ +public final class FedXZeroLengthPathEvaluationStep implements QueryEvaluationStep { + private final Var subjectVar; + private final Var objVar; + private final Var contextVar; + private final QueryValueEvaluationStep subPrep; + private final QueryValueEvaluationStep objPrep; + private final EvaluationStrategy strategy; + private final QueryEvaluationContext context; + + private final Supplier> statementSources; + private final Supplier queryInfo; + + public FedXZeroLengthPathEvaluationStep(Var subjectVar, Var objVar, Var contextVar, + QueryValueEvaluationStep subPrep, + QueryValueEvaluationStep objPrep, EvaluationStrategy strategy, QueryEvaluationContext context, + Supplier> statementSources, Supplier queryInfo) { + this.subjectVar = subjectVar; + this.objVar = objVar; + this.contextVar = contextVar; + this.subPrep = subPrep; + this.objPrep = objPrep; + this.strategy = strategy; + this.context = context; + + this.statementSources = statementSources; + this.queryInfo = queryInfo; + } + + @Override + public CloseableIteration evaluate(BindingSet bindings) { + Value subj = null; + try { + subj = subPrep.evaluate(bindings); + } catch (QueryEvaluationException ignored) { + } + + Value obj = null; + try { + obj = objPrep.evaluate(bindings); + } catch (QueryEvaluationException ignored) { + } + + if (subj != null && obj != null) { + if (!subj.equals(obj)) { + return EMPTY_ITERATION; + } + } + return getZeroLengthPathIterator(bindings, subjectVar, objVar, contextVar, subj, obj, context); + } + + protected FedXZeroLengthPathIteration getZeroLengthPathIterator(final BindingSet bindings, final Var subjectVar, + final Var objVar, final Var contextVar, Value subj, Value obj, QueryEvaluationContext context) { + return new FedXZeroLengthPathIteration(strategy, subjectVar, objVar, subj, obj, contextVar, bindings, context, + queryInfo.get(), statementSources.get()); + } +} 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 7617941bc38..94bccbfa04c 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 @@ -26,24 +26,32 @@ import org.eclipse.rdf4j.federated.algebra.CheckStatementPattern; import org.eclipse.rdf4j.federated.algebra.ConjunctiveFilterExpr; import org.eclipse.rdf4j.federated.algebra.EmptyResult; +import org.eclipse.rdf4j.federated.algebra.EmptyStatementPattern; import org.eclipse.rdf4j.federated.algebra.ExclusiveGroup; +import org.eclipse.rdf4j.federated.algebra.ExclusiveStatement; import org.eclipse.rdf4j.federated.algebra.ExclusiveTupleExpr; import org.eclipse.rdf4j.federated.algebra.ExclusiveTupleExprRenderer; +import org.eclipse.rdf4j.federated.algebra.FedXArbitraryLengthPath; import org.eclipse.rdf4j.federated.algebra.FedXLeftJoin; import org.eclipse.rdf4j.federated.algebra.FedXService; +import org.eclipse.rdf4j.federated.algebra.FedXZeroLengthPath; import org.eclipse.rdf4j.federated.algebra.FederatedDescribeOperator; import org.eclipse.rdf4j.federated.algebra.FilterExpr; import org.eclipse.rdf4j.federated.algebra.FilterValueExpr; +import org.eclipse.rdf4j.federated.algebra.HolderNode; import org.eclipse.rdf4j.federated.algebra.NJoin; import org.eclipse.rdf4j.federated.algebra.NUnion; import org.eclipse.rdf4j.federated.algebra.SingleSourceQuery; import org.eclipse.rdf4j.federated.algebra.StatementSource; +import org.eclipse.rdf4j.federated.algebra.StatementSource.StatementSourceType; +import org.eclipse.rdf4j.federated.algebra.StatementSourcePattern; import org.eclipse.rdf4j.federated.algebra.StatementTupleExpr; import org.eclipse.rdf4j.federated.cache.CacheUtils; import org.eclipse.rdf4j.federated.cache.SourceSelectionCache; import org.eclipse.rdf4j.federated.endpoint.Endpoint; import org.eclipse.rdf4j.federated.evaluation.concurrent.ControlledWorkerScheduler; import org.eclipse.rdf4j.federated.evaluation.concurrent.ParallelServiceExecutor; +import org.eclipse.rdf4j.federated.evaluation.iterator.FedXPathIteration; import org.eclipse.rdf4j.federated.evaluation.iterator.FederatedDescribeIteration; import org.eclipse.rdf4j.federated.evaluation.iterator.SingleBindingSetIteration; import org.eclipse.rdf4j.federated.evaluation.join.ControlledWorkerBoundJoin; @@ -89,6 +97,7 @@ import org.eclipse.rdf4j.query.algebra.Join; import org.eclipse.rdf4j.query.algebra.QueryRoot; import org.eclipse.rdf4j.query.algebra.Service; +import org.eclipse.rdf4j.query.algebra.StatementPattern; import org.eclipse.rdf4j.query.algebra.TupleExpr; import org.eclipse.rdf4j.query.algebra.ValueExpr; import org.eclipse.rdf4j.query.algebra.Var; @@ -271,11 +280,95 @@ protected Set performSourceSelection(List members, SourceSel QueryInfo queryInfo, GenericInfoOptimizer info) { + Set relevantMembers = new HashSet<>(); + var stmtPatterns = new HashSet<>(info.getStatements()); + + if (info.hasPathExpression()) { + var handled = new HashSet<>(); + for (StatementPattern stmt : stmtPatterns) { + if (stmt.getParentNode() instanceof FedXArbitraryLengthPath) { + var pathExpr = (FedXArbitraryLengthPath) stmt.getParentNode(); + var identifiedMembers = performSourceSelection(pathExpr, stmt, members, cache, queryInfo); + relevantMembers.addAll(identifiedMembers); + + handled.add(stmt); + } + } + stmtPatterns.removeAll(handled); + } + // Source Selection: all nodes are annotated with their source SourceSelection sourceSelection = new SourceSelection(members, cache, queryInfo); sourceSelection.doSourceSelection(info.getStatements()); - return sourceSelection.getRelevantSources(); + relevantMembers.addAll(sourceSelection.getRelevantSources()); + + return relevantMembers; + } + + /** + * Perform source selection on the statement pattern representing the path expression. Source selection must be a + * subset of the provided members. + * + * The implementation expects to replace the statement, e.g. with {@link StatementSourcePattern}, + * {@link ExclusiveStatement} or {@link EmptyStatementPattern} + * + * @param pathExpr + * @param stmt + * @param members + * @param cache + * @param queryInfo + * @return identified relevant members + */ + protected Set performSourceSelection(FedXArbitraryLengthPath pathExpr, StatementPattern stmt, + List members, SourceSelectionCache cache, + QueryInfo queryInfo) { + + // for now the strategy is to do source selection on a statement pattern containing the predicate + // of the original path expression. + // Note: for zero-length path we add all members (as there does not have to exist a path with the + // predicate) + Set identifiedMembers; + if (pathExpr.getMinLength() == 0) { + identifiedMembers = new HashSet<>(members); + } else { + StatementPattern checkStmt = new StatementPattern(stmt.getScope(), new Var("subject"), + clone(stmt.getPredicateVar()), new Var("object"), clone(stmt.getContextVar())); + @SuppressWarnings("unused") // only used as artificial parent + HolderNode holderParent = new HolderNode(checkStmt); + + SourceSelection sourceSelection = new SourceSelection(members, cache, queryInfo); + sourceSelection.doSourceSelection(List.of(checkStmt)); + + identifiedMembers = sourceSelection.getRelevantSources(); + } + + var statementSources = identifiedMembers.stream() + .map(e -> new StatementSource(e.getId(), StatementSourceType.REMOTE)) + .collect(Collectors.toList()); + + if (statementSources.size() == 1) { + var replacement = new ExclusiveStatement(stmt, statementSources.iterator().next(), queryInfo); + stmt.replaceWith(replacement); + } else if (statementSources.size() > 1) { + var replacement = new StatementSourcePattern(stmt, queryInfo); + for (var stmtSource : statementSources) { + ((StatementSourcePattern) replacement).addStatementSource(stmtSource); + } + stmt.replaceWith(replacement); + } else { + var replacement = new EmptyStatementPattern(stmt); + stmt.replaceWith(replacement); + } + + return identifiedMembers; + } + + private Var clone(Var var) { + if (var == null) { + return null; + } + return var.clone(); } protected void optimizeJoinOrder(TupleExpr query, QueryInfo queryInfo, GenericInfoOptimizer info) { @@ -351,6 +444,14 @@ public CloseableIteration evaluate( return evaluateService((FedXService) expr, bindings); } + if (expr instanceof FedXArbitraryLengthPath) { + return evaluateArbitrayLengthPath((FedXArbitraryLengthPath) expr, bindings); + } + + if (expr instanceof FedXZeroLengthPath) { + return evaluateZeroLengthPath((FedXZeroLengthPath) expr, bindings); + } + if (expr instanceof EmptyResult) { return new EmptyIteration<>(); } @@ -397,6 +498,14 @@ public QueryEvaluationStep precompile( return QueryEvaluationStep.minimal(this, expr); } + if (expr instanceof FedXArbitraryLengthPath) { + return prepare((FedXArbitraryLengthPath) expr, context); + } + + if (expr instanceof FedXZeroLengthPath) { + return prepare((FedXZeroLengthPath) expr, context); + } + if (expr instanceof EmptyResult) { return QueryEvaluationStep.minimal(this, expr); } @@ -528,6 +637,40 @@ protected QueryEvaluationStep prepareNJoin(NJoin join, QueryEvaluationContext co } + protected CloseableIteration evaluateArbitrayLengthPath( + FedXArbitraryLengthPath alp, BindingSet bindings) + throws QueryEvaluationException { + + return precompile(alp).evaluate(bindings); + } + + protected QueryEvaluationStep prepare(FedXArbitraryLengthPath alp, QueryEvaluationContext context) + throws QueryEvaluationException { + return (bindings) -> new FedXPathIteration(FederationEvalStrategy.this, alp.getScope(), alp.getSubjectVar(), + alp.getPathExpression(), alp.getObjectVar(), alp.getContextVar(), alp.getMinLength(), bindings, + alp.getQueryInfo()); + } + + protected CloseableIteration evaluateZeroLengthPath(FedXZeroLengthPath zlp, + BindingSet bindings) + throws QueryEvaluationException { + + return precompile(zlp).evaluate(bindings); + } + + protected QueryEvaluationStep prepare(FedXZeroLengthPath zlp, QueryEvaluationContext context) + throws QueryEvaluationException { + + final Var subjectVar = zlp.getSubjectVar(); + final Var objVar = zlp.getObjectVar(); + final Var contextVar = zlp.getContextVar(); + QueryValueEvaluationStep subPrep = precompile(subjectVar, context); + QueryValueEvaluationStep objPrep = precompile(objVar, context); + + return new FedXZeroLengthPathEvaluationStep(subjectVar, objVar, contextVar, subPrep, objPrep, this, context, + () -> zlp.getStatementSources(), () -> zlp.getQueryInfo()); + } + /** * Evaluate a {@link FedXLeftJoin} (i.e. an OPTIONAL clause) * diff --git a/tools/federation/src/main/java/org/eclipse/rdf4j/federated/evaluation/iterator/FedXPathIteration.java b/tools/federation/src/main/java/org/eclipse/rdf4j/federated/evaluation/iterator/FedXPathIteration.java new file mode 100644 index 00000000000..fd69e5c9a43 --- /dev/null +++ b/tools/federation/src/main/java/org/eclipse/rdf4j/federated/evaluation/iterator/FedXPathIteration.java @@ -0,0 +1,486 @@ +/******************************************************************************* + * Copyright (c) 2024 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.federated.evaluation.iterator; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Queue; +import java.util.Set; + +import org.eclipse.rdf4j.common.iteration.CloseableIteration; +import org.eclipse.rdf4j.common.iteration.Iterations; +import org.eclipse.rdf4j.common.iteration.LookAheadIteration; +import org.eclipse.rdf4j.federated.algebra.FedXZeroLengthPath; +import org.eclipse.rdf4j.federated.algebra.StatementSource; +import org.eclipse.rdf4j.federated.algebra.StatementTupleExpr; +import org.eclipse.rdf4j.federated.structures.QueryInfo; +import org.eclipse.rdf4j.model.Value; +import org.eclipse.rdf4j.query.BindingSet; +import org.eclipse.rdf4j.query.MutableBindingSet; +import org.eclipse.rdf4j.query.QueryEvaluationException; +import org.eclipse.rdf4j.query.algebra.QueryModelNode; +import org.eclipse.rdf4j.query.algebra.StatementPattern.Scope; +import org.eclipse.rdf4j.query.algebra.TupleExpr; +import org.eclipse.rdf4j.query.algebra.Var; +import org.eclipse.rdf4j.query.algebra.evaluation.EvaluationStrategy; +import org.eclipse.rdf4j.query.algebra.evaluation.QueryBindingSet; +import org.eclipse.rdf4j.query.algebra.evaluation.iterator.PathIteration; +import org.eclipse.rdf4j.query.algebra.helpers.AbstractQueryModelVisitor; + +/** + * A iteration to evaluate property path expressions. + * + * @see PathIteration + */ +public class FedXPathIteration extends LookAheadIteration { + + /* + * IMPL NOTE: + * + * This is technically almost a 1:1 copy of org.eclipse.rdf4j.query.algebra.evaluation.iterator.PathIteration. + * Reusing or extending PathIteration requires refactoring in its constructor initialization. + * + * The main difference is in keeping track of QueryInfo and creating a FedXZeroLengthPath in #createIteration for + * zero length path expressions. + */ + + /** + * + */ + private final EvaluationStrategy strategy; + + private long currentLength; + + private CloseableIteration currentIter; + + private final BindingSet bindings; + + private final Scope scope; + + private final Var startVar; + + private final Var endVar; + + private final boolean startVarFixed; + + private final boolean endVarFixed; + + private final Queue valueQueue; + + private final Set reportedValues; + + private final Set unreportedValues; + + private final TupleExpr pathExpression; + + private final Var contextVar; + + private ValuePair currentVp; + + private static final String JOINVAR_PREFIX = "intermediate_join_"; + + private final Set namedIntermediateJoins = new HashSet<>(); + + private final QueryInfo queryInfo; + + public FedXPathIteration(EvaluationStrategy strategy, Scope scope, Var startVar, TupleExpr pathExpression, + Var endVar, Var contextVar, long minLength, BindingSet bindings, QueryInfo queryInfo) + throws QueryEvaluationException { + + this.strategy = strategy; + this.scope = scope; + this.startVar = startVar; + this.endVar = endVar; + + this.startVarFixed = startVar.hasValue() || bindings.hasBinding(startVar.getName()); + this.endVarFixed = endVar.hasValue() || bindings.hasBinding(endVar.getName()); + + this.pathExpression = pathExpression; + this.contextVar = contextVar; + + this.currentLength = minLength; + this.bindings = bindings; + + this.reportedValues = strategy.makeSet(); + this.unreportedValues = strategy.makeSet(); + this.valueQueue = strategy.makeQueue(); + + this.queryInfo = queryInfo; + + createIteration(); + + } + + @Override + protected BindingSet getNextElement() throws QueryEvaluationException { + again: while (true) { + while (currentIter != null && !currentIter.hasNext()) { + Iterations.closeCloseable(currentIter); + createIteration(); + // stop condition: if the iter is an EmptyIteration + if (currentIter == null) { + break; + } + } + + while (currentIter != null && currentIter.hasNext()) { + BindingSet potentialNextElement = currentIter.next(); + QueryBindingSet nextElement; + // if it is not a compatible type of BindingSet + if (potentialNextElement instanceof QueryBindingSet) { + nextElement = (QueryBindingSet) potentialNextElement; + } else { + nextElement = new QueryBindingSet(potentialNextElement); + } + + if (!startVarFixed && !endVarFixed && currentVp != null) { + Value startValue = currentVp.getStartValue(); + + if (startValue != null) { + nextElement = new QueryBindingSet(nextElement); + addBinding(nextElement, startVar.getName(), startValue); + } + } + + ValuePair vp = valuePairFromStartAndEnd(nextElement); + + if (!isCyclicPath(vp)) { + + if (reportedValues.contains(vp)) { + // new arbitrary-length path semantics: filter out + // duplicates + if (currentIter.hasNext()) { + continue; + } else { + // if the current iter is exhausted, we need to check + // that no further paths of greater length still exists. + continue again; + } + } + + if (startVarFixed && endVarFixed) { + Value endValue = getVarValue(endVar, endVarFixed, nextElement); + if (endValue.equals(vp.endValue)) { + add(reportedValues, vp); + if (!vp.startValue.equals(vp.endValue)) { + addToQueue(valueQueue, vp); + } + if (!nextElement.hasBinding(startVar.getName())) { + addBinding(nextElement, startVar.getName(), vp.startValue); + } + if (!nextElement.hasBinding(endVar.getName())) { + addBinding(nextElement, endVar.getName(), vp.endValue); + } + return removeIntermediateJoinVars(nextElement); + } else { + if (add(unreportedValues, vp)) { + if (!vp.startValue.equals(vp.endValue)) { + addToQueue(valueQueue, vp); + } + } + continue again; + } + } else { + add(reportedValues, vp); + if (!vp.startValue.equals(vp.endValue)) { + addToQueue(valueQueue, vp); + } + if (!nextElement.hasBinding(startVar.getName())) { + addBinding(nextElement, startVar.getName(), vp.startValue); + } + if (!nextElement.hasBinding(endVar.getName())) { + addBinding(nextElement, endVar.getName(), vp.endValue); + } + return removeIntermediateJoinVars(nextElement); + } + } else { + continue again; + } + } + + // if we're done, throw away the cached lists of values to avoid + // hogging resources + reportedValues.clear(); + unreportedValues.clear(); + valueQueue.clear(); + return null; + } + } + + private BindingSet removeIntermediateJoinVars(QueryBindingSet nextElement) { + nextElement.removeAll(namedIntermediateJoins); + return nextElement; + } + + private ValuePair valuePairFromStartAndEnd(MutableBindingSet nextElement) { + Value v1, v2; + + if (startVarFixed && endVarFixed && currentLength > 2) { + v1 = getVarValue(startVar, startVarFixed, nextElement); + v2 = nextElement.getValue("END_" + JOINVAR_PREFIX + this.hashCode()); + } else if (startVarFixed && endVarFixed && currentLength == 2) { + v1 = getVarValue(startVar, startVarFixed, nextElement); + v2 = nextElement.getValue(JOINVAR_PREFIX + (currentLength - 1) + "_" + this.hashCode()); + } else { + v1 = getVarValue(startVar, startVarFixed, nextElement); + v2 = getVarValue(endVar, endVarFixed, nextElement); + } + return new ValuePair(v1, v2); + } + + private void addBinding(MutableBindingSet bs, String name, Value value) { + bs.addBinding(name, value); + } + + @Override + protected void handleClose() throws QueryEvaluationException { + try { + super.handleClose(); + } finally { + Iterations.closeCloseable(currentIter); + } + + } + + /** + * @param valueQueue2 + * @param vp + */ + protected boolean addToQueue(Queue valueQueue2, ValuePair vp) throws QueryEvaluationException { + return valueQueue2.add(vp); + } + + /** + * @param valueSet + * @param vp + */ + protected boolean add(Set valueSet, ValuePair vp) throws QueryEvaluationException { + return valueSet.add(vp); + } + + private Value getVarValue(Var var, boolean fixedValue, BindingSet bindingSet) { + Value v; + if (fixedValue) { + v = var.getValue(); + if (v == null) { + v = this.bindings.getValue(var.getName()); + } + } else { + v = bindingSet.getValue(var.getName()); + } + + return v; + } + + private boolean isCyclicPath(ValuePair vp) { + if (currentLength <= 2) { + return false; + } + + return reportedValues.contains(vp); + + } + + private void createIteration() throws QueryEvaluationException { + + if (isUnbound(startVar, bindings) || isUnbound(endVar, bindings)) { + // the variable must remain unbound for this solution see https://www.w3.org/TR/sparql11-query/#assignment + currentIter = null; + } else if (currentLength == 0L) { + + // For the federation we need to path through statement sources and query info + + // determine statement sources relevant in the current path scope + var statementSources = new ArrayList(); + if (pathExpression instanceof StatementTupleExpr) { + statementSources.addAll(((StatementTupleExpr) pathExpression).getStatementSources()); + } + + FedXZeroLengthPath zlp = new FedXZeroLengthPath(scope, startVar.clone(), endVar.clone(), + contextVar != null ? contextVar.clone() : null, queryInfo, statementSources); + + currentIter = this.strategy.evaluate(zlp, bindings); + currentLength++; + } else if (currentLength == 1) { + TupleExpr pathExprClone = pathExpression.clone(); + + if (startVarFixed && endVarFixed) { + Var replacement = createAnonVar(JOINVAR_PREFIX + currentLength + "_" + this.hashCode()); + + VarReplacer replacer = new VarReplacer(endVar, replacement, 0, false); + pathExprClone.visit(replacer); + } + currentIter = this.strategy.evaluate(pathExprClone, bindings); + currentLength++; + } else { + + currentVp = valueQueue.poll(); + + if (currentVp != null) { + + TupleExpr pathExprClone = pathExpression.clone(); + + if (startVarFixed && endVarFixed) { + + Var startReplacement = createAnonVar(JOINVAR_PREFIX + currentLength + "_" + this.hashCode()); + Var endReplacement = createAnonVar("END_" + JOINVAR_PREFIX + this.hashCode()); + startReplacement.setAnonymous(false); + endReplacement.setAnonymous(false); + + Value v = currentVp.getEndValue(); + startReplacement.setValue(v); + + VarReplacer replacer = new VarReplacer(startVar, startReplacement, 0, false); + pathExprClone.visit(replacer); + + replacer = new VarReplacer(endVar, endReplacement, 0, false); + pathExprClone.visit(replacer); + } else { + Var toBeReplaced; + Value v; + if (!endVarFixed) { + toBeReplaced = startVar; + v = currentVp.getEndValue(); + } else { + toBeReplaced = endVar; + v = currentVp.getStartValue(); + } + + Var replacement = createAnonVar(JOINVAR_PREFIX + currentLength + "-" + this.hashCode()); + replacement.setValue(v); + + VarReplacer replacer = new VarReplacer(toBeReplaced, replacement, 0, false); + pathExprClone.visit(replacer); + } + + currentIter = this.strategy.evaluate(pathExprClone, bindings); + } else { + currentIter = null; + } + currentLength++; + + } + } + + protected boolean isUnbound(Var var, BindingSet bindings) { + if (var == null) { + return false; + } else { + return bindings.hasBinding(var.getName()) && bindings.getValue(var.getName()) == null; + } + } + + protected static class ValuePair { + + private final Value startValue; + + private final Value endValue; + + public ValuePair(Value startValue, Value endValue) { + this.startValue = startValue; + this.endValue = endValue; + } + + /** + * @return Returns the startValue. + */ + public Value getStartValue() { + return startValue; + } + + /** + * @return Returns the endValue. + */ + public Value getEndValue() { + return endValue; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((endValue == null) ? 0 : endValue.hashCode()); + result = prime * result + ((startValue == null) ? 0 : startValue.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof ValuePair)) { + return false; + } + ValuePair other = (ValuePair) obj; + if (endValue == null) { + if (other.endValue != null) { + return false; + } + } else if (!endValue.equals(other.endValue)) { + return false; + } + if (startValue == null) { + if (other.startValue != null) { + return false; + } + } else if (!startValue.equals(other.startValue)) { + return false; + } + return true; + } + } + + class VarReplacer extends AbstractQueryModelVisitor { + + private final Var toBeReplaced; + + private final Var replacement; + + private final long index; + + private final boolean replaceAnons; + + public VarReplacer(Var toBeReplaced, Var replacement, long index, boolean replaceAnons) { + this.toBeReplaced = toBeReplaced; + this.replacement = replacement; + this.index = index; + this.replaceAnons = replaceAnons; + } + + @Override + public void meet(Var var) { + if (toBeReplaced.equals(var) || (toBeReplaced.isAnonymous() && var.isAnonymous() + && (toBeReplaced.hasValue() && toBeReplaced.getValue().equals(var.getValue())))) { + QueryModelNode parent = var.getParentNode(); + parent.replaceChildNode(var, replacement.clone()); + } else if (replaceAnons && var.isAnonymous() && !var.hasValue()) { + Var replacementVar = createAnonVar("anon-replace-" + var.getName() + index); + QueryModelNode parent = var.getParentNode(); + parent.replaceChildNode(var, replacementVar); + } + } + + } + + private Var createAnonVar(String varName, Value v, boolean anonymous) { + namedIntermediateJoins.add(varName); + return new Var(varName, null, anonymous, false); + } + + public Var createAnonVar(String varName) { + return createAnonVar(varName, null, true); + } + +} diff --git a/tools/federation/src/main/java/org/eclipse/rdf4j/federated/evaluation/iterator/FedXZeroLengthPathIteration.java b/tools/federation/src/main/java/org/eclipse/rdf4j/federated/evaluation/iterator/FedXZeroLengthPathIteration.java new file mode 100644 index 00000000000..83025d13693 --- /dev/null +++ b/tools/federation/src/main/java/org/eclipse/rdf4j/federated/evaluation/iterator/FedXZeroLengthPathIteration.java @@ -0,0 +1,220 @@ +/******************************************************************************* + * Copyright (c) 2024 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.federated.evaluation.iterator; + +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.function.BiConsumer; + +import org.eclipse.rdf4j.common.iteration.CloseableIteration; +import org.eclipse.rdf4j.common.iteration.LookAheadIteration; +import org.eclipse.rdf4j.federated.algebra.EmptyStatementPattern; +import org.eclipse.rdf4j.federated.algebra.ExclusiveStatement; +import org.eclipse.rdf4j.federated.algebra.FedXZeroLengthPath; +import org.eclipse.rdf4j.federated.algebra.StatementSource; +import org.eclipse.rdf4j.federated.algebra.StatementSourcePattern; +import org.eclipse.rdf4j.federated.structures.QueryInfo; +import org.eclipse.rdf4j.model.Literal; +import org.eclipse.rdf4j.model.Value; +import org.eclipse.rdf4j.model.impl.SimpleValueFactory; +import org.eclipse.rdf4j.query.BindingSet; +import org.eclipse.rdf4j.query.MutableBindingSet; +import org.eclipse.rdf4j.query.QueryEvaluationException; +import org.eclipse.rdf4j.query.algebra.StatementPattern; +import org.eclipse.rdf4j.query.algebra.StatementPattern.Scope; +import org.eclipse.rdf4j.query.algebra.TupleExpr; +import org.eclipse.rdf4j.query.algebra.Var; +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.impl.QueryEvaluationContext; +import org.eclipse.rdf4j.query.algebra.evaluation.iterator.CrossProductIteration; +import org.eclipse.rdf4j.query.algebra.evaluation.iterator.ZeroLengthPathIteration; + +/** + * An iteration to evaluated {@link FedXZeroLengthPath} + * + * @see ZeroLengthPathIteration + */ +public class FedXZeroLengthPathIteration extends LookAheadIteration { + + /* + * IMPL NOTE: + * + * This is technically almost a 1:1 copy of + * org.eclipse.rdf4j.query.algebra.evaluation.iterator.ZeroLengthPathIteration Reusing or extending + * ZeroLengthPathIteration requires refactoring in its constructor initialization. + * + * The main difference is in keeping track of QueryInfo and statement sources to be used in the precompiled + * statement. Additionally the variable names for anon vars are renamed + */ + + private static final Literal OBJECT = SimpleValueFactory.getInstance().createLiteral("object"); + + private static final Literal SUBJECT = SimpleValueFactory.getInstance().createLiteral("subject"); + + // Note: in contrast to the original zero length path iteration we use "_" instead of "-" + // as we need variable names valid in SELECT queries + + private static final String ANON_SUBJECT_VAR = "zero_length_internal_start"; + + private static final String ANON_PREDICATE_VAR = "zero_length_internal_pred"; + + private static final String ANON_OBJECT_VAR = "zero_length_internal_end"; + + private static final String ANON_SEQUENCE_VAR = "zero_length_internal_seq"; + + private QueryBindingSet result; + + private final Value subj; + + private final Value obj; + + private final BindingSet bindings; + + private CloseableIteration iter; + + private Set reportedValues; + + private final Var contextVar; + + private final EvaluationStrategy evaluationStrategy; + + private final QueryEvaluationStep precompile; + + private final QueryEvaluationContext context; + + private final BiConsumer setSubject; + + private final BiConsumer setObject; + + private final BiConsumer setContext; + + public FedXZeroLengthPathIteration(EvaluationStrategy evaluationStrategyImpl, Var subjectVar, Var objVar, + Value subj, Value obj, Var contextVar, BindingSet bindings, QueryEvaluationContext context, + QueryInfo queryInfo, List statementSources) { + this.evaluationStrategy = evaluationStrategyImpl; + this.context = context; + this.result = new QueryBindingSet(bindings); + this.contextVar = contextVar; + this.subj = subj; + this.obj = obj; + this.bindings = bindings; + Var startVar = createAnonVar(ANON_SUBJECT_VAR); + Var predicate = createAnonVar(ANON_PREDICATE_VAR); + Var endVar = createAnonVar(ANON_OBJECT_VAR); + + StatementPattern subjects; + if (contextVar != null) { + subjects = new StatementPattern(Scope.NAMED_CONTEXTS, startVar, predicate, endVar, contextVar.clone()); + } else { + subjects = new StatementPattern(startVar, predicate, endVar); + } + + // specialization for federation: we need to attach statement sources + // to the precompiled expr + TupleExpr expr; + if (statementSources.size() == 1) { + expr = new ExclusiveStatement(subjects, statementSources.get(0), queryInfo); + } else if (statementSources.size() > 1) { + expr = new StatementSourcePattern(subjects, queryInfo); + for (var stmtSource : statementSources) { + ((StatementSourcePattern) expr).addStatementSource(stmtSource); + } + } else { + expr = new EmptyStatementPattern(subjects); + } + precompile = evaluationStrategy.precompile(expr, context); + + setSubject = context.addBinding(subjectVar.getName()); + setObject = context.addBinding(objVar.getName()); + if (contextVar != null) { + setContext = context.addBinding(contextVar.getName()); + } else { + setContext = null; + } + + } + + @Override + protected BindingSet getNextElement() throws QueryEvaluationException { + if (subj == null && obj == null) { + if (this.reportedValues == null) { + reportedValues = evaluationStrategy.makeSet(); + } + if (this.iter == null) { + // join with a sequence so we iterate over every entry twice + QueryBindingSet bs1 = new QueryBindingSet(1); + bs1.addBinding(ANON_SEQUENCE_VAR, SUBJECT); + QueryBindingSet bs2 = new QueryBindingSet(1); + bs2.addBinding(ANON_SEQUENCE_VAR, OBJECT); + List seqList = Arrays.asList(bs1, bs2); + iter = new CrossProductIteration(createIteration(), seqList); + } + + while (iter.hasNext()) { + BindingSet bs = iter.next(); + + boolean isSubjOrObj = bs.getValue(ANON_SEQUENCE_VAR).stringValue().equals("subject"); + String endpointVarName = isSubjOrObj ? ANON_SUBJECT_VAR : ANON_OBJECT_VAR; + Value v = bs.getValue(endpointVarName); + + if (reportedValues.add(v)) { + MutableBindingSet next = context.createBindingSet(bindings); + setSubject.accept(v, next); + setObject.accept(v, next); + if (setContext != null) { + Value context = bs.getValue(contextVar.getName()); + if (context != null) { + setContext.accept(context, next); + } + } + return next; + } + } + iter.close(); + + // if we're done, throw away the cached list of values to avoid hogging + // resources + reportedValues = null; + return null; + } else { + if (result != null) { + if (obj == null && subj != null) { + setObject.accept(subj, result); + } else if (subj == null && obj != null) { + setSubject.accept(obj, result); + } else if (subj != null && subj.equals(obj)) { + // empty bindings + // (result but nothing to bind as subjectVar and objVar are both fixed) + } else { + result = null; + } + } + + QueryBindingSet next = result; + result = null; + return next; + } + } + + private CloseableIteration createIteration() throws QueryEvaluationException { + CloseableIteration iter = precompile.evaluate(bindings); + return iter; + } + + public Var createAnonVar(String varName) { + Var var = new Var(varName); + var.setAnonymous(true); + return var; + } +} diff --git a/tools/federation/src/main/java/org/eclipse/rdf4j/federated/optimizer/ExclusiveTupleExprOptimizer.java b/tools/federation/src/main/java/org/eclipse/rdf4j/federated/optimizer/ExclusiveTupleExprOptimizer.java index 01f52fe6dc4..dd89480d741 100644 --- a/tools/federation/src/main/java/org/eclipse/rdf4j/federated/optimizer/ExclusiveTupleExprOptimizer.java +++ b/tools/federation/src/main/java/org/eclipse/rdf4j/federated/optimizer/ExclusiveTupleExprOptimizer.java @@ -13,8 +13,10 @@ import org.eclipse.rdf4j.federated.algebra.ExclusiveArbitraryLengthPath; import org.eclipse.rdf4j.federated.algebra.ExclusiveStatement; import org.eclipse.rdf4j.federated.algebra.ExclusiveTupleExpr; +import org.eclipse.rdf4j.federated.algebra.FedXArbitraryLengthPath; import org.eclipse.rdf4j.federated.exception.OptimizationException; import org.eclipse.rdf4j.query.algebra.ArbitraryLengthPath; +import org.eclipse.rdf4j.query.algebra.QueryModelNode; import org.eclipse.rdf4j.query.algebra.Service; import org.eclipse.rdf4j.query.algebra.TupleExpr; import org.eclipse.rdf4j.query.algebra.helpers.AbstractSimpleQueryModelVisitor; @@ -37,6 +39,14 @@ public void optimize(TupleExpr tupleExpr) { tupleExpr.visit(this); } + @Override + public void meetOther(QueryModelNode node) throws OptimizationException { + if (node instanceof FedXArbitraryLengthPath) { + meet((FedXArbitraryLengthPath) node); + } + super.meetOther(node); + } + @Override public void meet(ArbitraryLengthPath node) throws OptimizationException { @@ -50,6 +60,16 @@ public void meet(ArbitraryLengthPath node) throws OptimizationException { super.meet(node); } + protected void meet(FedXArbitraryLengthPath node) { + if (node.getPathExpression() instanceof ExclusiveStatement) { + ExclusiveStatement st = (ExclusiveStatement) node.getPathExpression(); + ExclusiveArbitraryLengthPath eNode = new ExclusiveArbitraryLengthPath(node, st.getOwner(), + st.getQueryInfo()); + node.replaceWith(eNode); + return; + } + } + @Override public void meet(Service node) throws OptimizationException { // do not optimize anything within SERVICE diff --git a/tools/federation/src/main/java/org/eclipse/rdf4j/federated/optimizer/GenericInfoOptimizer.java b/tools/federation/src/main/java/org/eclipse/rdf4j/federated/optimizer/GenericInfoOptimizer.java index 227a7e39c10..8983275647c 100644 --- a/tools/federation/src/main/java/org/eclipse/rdf4j/federated/optimizer/GenericInfoOptimizer.java +++ b/tools/federation/src/main/java/org/eclipse/rdf4j/federated/optimizer/GenericInfoOptimizer.java @@ -14,11 +14,13 @@ import java.util.Collections; import java.util.List; +import org.eclipse.rdf4j.federated.algebra.FedXArbitraryLengthPath; import org.eclipse.rdf4j.federated.algebra.FedXLeftJoin; import org.eclipse.rdf4j.federated.algebra.FederatedDescribeOperator; import org.eclipse.rdf4j.federated.algebra.NJoin; import org.eclipse.rdf4j.federated.exception.OptimizationException; import org.eclipse.rdf4j.federated.structures.QueryInfo; +import org.eclipse.rdf4j.query.algebra.ArbitraryLengthPath; import org.eclipse.rdf4j.query.algebra.DescribeOperator; import org.eclipse.rdf4j.query.algebra.Filter; import org.eclipse.rdf4j.query.algebra.Join; @@ -45,6 +47,7 @@ public class GenericInfoOptimizer extends AbstractSimpleQueryModelVisitor services = null; protected long limit = -1; // set to a positive number if the main query has a limit protected List stmts = new ArrayList<>(); @@ -67,6 +70,10 @@ public boolean hasUnion() { return hasUnion; } + public boolean hasPathExpression() { + return hasPathExpr; + } + public List getStatements() { return stmts; } @@ -141,6 +148,16 @@ public void meet(LeftJoin node) throws OptimizationException { node.replaceWith(join); } + @Override + public void meet(ArbitraryLengthPath node) throws OptimizationException { + + FedXArbitraryLengthPath falp = new FedXArbitraryLengthPath(node, queryInfo); + falp.visitChildren(this); + + node.replaceWith(falp); + hasPathExpr = true; + } + @Override public void meet(StatementPattern node) { stmts.add(node); diff --git a/tools/federation/src/test/java/org/eclipse/rdf4j/federated/PropertyPathTests.java b/tools/federation/src/test/java/org/eclipse/rdf4j/federated/PropertyPathTests.java index 8ba686de06b..fc83c2bf10a 100644 --- a/tools/federation/src/test/java/org/eclipse/rdf4j/federated/PropertyPathTests.java +++ b/tools/federation/src/test/java/org/eclipse/rdf4j/federated/PropertyPathTests.java @@ -14,12 +14,17 @@ import java.util.List; import org.eclipse.rdf4j.common.iteration.Iterations; +import org.eclipse.rdf4j.model.util.Values; import org.eclipse.rdf4j.model.vocabulary.FOAF; import org.eclipse.rdf4j.model.vocabulary.OWL; import org.eclipse.rdf4j.model.vocabulary.RDFS; import org.eclipse.rdf4j.model.vocabulary.SKOS; import org.eclipse.rdf4j.query.BindingSet; +import org.eclipse.rdf4j.query.QueryResults; +import org.eclipse.rdf4j.query.TupleQuery; import org.eclipse.rdf4j.query.TupleQueryResult; +import org.eclipse.rdf4j.repository.Repository; +import org.eclipse.rdf4j.repository.RepositoryConnection; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -105,20 +110,12 @@ public void testPropertyPath_ExclusiveGroup() throws Exception { } @Test - public void testPropertyPath_ExclusivePath() throws Exception { + public void testPropertyPath_Combination() throws Exception { prepareTest(Arrays.asList("/tests/propertypath/data1.ttl", "/tests/propertypath/data2.ttl")); String query = "SELECT * WHERE { ?x rdf:type/rdfs:subClassOf* foaf:Agent . ?x rdfs:label ?label}"; - String actualQueryPlan = federationContext().getQueryManager().getQueryPlan(query); - - // Note: we currently cannot compare the query plan, because the queryplan contains generated - // variable name identifiers for anonymous nodes. -// assertQueryPlanEquals(readResourceAsString("/tests/propertypath/query_path_exclusivePath.qp"), -// actualQueryPlan); - Assertions.assertTrue(actualQueryPlan.contains("ExclusiveArbitraryLengthPath")); - try (TupleQueryResult tqr = federationContext().getQueryManager().prepareTupleQuery(query).evaluate()) { List res = Iterations.asList(tqr); @@ -143,4 +140,213 @@ public void testPropertyPath_BoundInJoin() throws Exception { } + @Test + public void testZeroLengthPath_length1() throws Exception { + + prepareTest(Arrays.asList("/tests/basic/data_emptyStore.ttl", "/tests/basic/data_emptyStore.ttl")); + + Repository repo1 = getRepository(1); + Repository repo2 = getRepository(2); + + try (RepositoryConnection con = repo1.getConnection()) { + con.add(Values.iri("http://example.org/A"), RDFS.SUBCLASSOF, Values.iri("http://example.org/B")); + } + + try (RepositoryConnection con = repo2.getConnection()) { + con.add(Values.iri("http://example.org/X"), RDFS.SUBCLASSOF, Values.iri("http://example.org/Y")); + } + + Repository fedxRepo = fedxRule.getRepository(); + + // 1: variable subject & object + try (RepositoryConnection con = fedxRepo.getConnection()) { + TupleQuery tupleQuery = con.prepareTupleQuery( + "PREFIX rdfs: " + + "SELECT * WHERE { " + + " ?subClass (rdfs:subClassOf*) ?myClass. " + + " } " + ); + + Assertions.assertEquals(6, QueryResults.asSet(tupleQuery.evaluate()).size()); + } + + // 2: bound (matching) object + try (RepositoryConnection con = fedxRepo.getConnection()) { + TupleQuery tupleQuery = con.prepareTupleQuery( + "PREFIX rdfs: " + + "SELECT * WHERE { " + + " ?subClass (rdfs:subClassOf*) ?myClass. " + + " } " + ); + tupleQuery.setBinding("myClass", Values.iri("http://example.org/B")); + + Assertions.assertEquals(2, QueryResults.asSet(tupleQuery.evaluate()).size()); + } + + // 3: bound (non-matching) object + try (RepositoryConnection con = fedxRepo.getConnection()) { + TupleQuery tupleQuery = con.prepareTupleQuery( + "PREFIX rdfs: " + + "PREFIX ex: " + + "SELECT * WHERE { " + + " ?subClass (ex:something*) ?myClass. " + + " } " + ); + tupleQuery.setBinding("myClass", Values.iri("http://example.org/B")); + + Assertions.assertEquals(1, QueryResults.asSet(tupleQuery.evaluate()).size()); + } + } + + @Test + public void testZeroLengthPath_length2() throws Exception { + + prepareTest(Arrays.asList("/tests/basic/data_emptyStore.ttl", "/tests/basic/data_emptyStore.ttl")); + + Repository repo1 = getRepository(1); + Repository repo2 = getRepository(2); + + try (RepositoryConnection con = repo1.getConnection()) { + con.add(Values.iri("http://example.org/A"), RDFS.SUBCLASSOF, Values.iri("http://example.org/B")); + con.add(Values.iri("http://example.org/B"), RDFS.SUBCLASSOF, Values.iri("http://example.org/C")); + } + + try (RepositoryConnection con = repo2.getConnection()) { + con.add(Values.iri("http://example.org/X"), RDFS.SUBCLASSOF, Values.iri("http://example.org/Y")); + } + + Repository fedxRepo = fedxRule.getRepository(); + + // 1: variable subject & object + try (RepositoryConnection con = fedxRepo.getConnection()) { + TupleQuery tupleQuery = con.prepareTupleQuery( + "PREFIX rdfs: " + + "SELECT * WHERE { " + + " ?subClass (rdfs:subClassOf*) ?myClass. " + + " } " + ); + + Assertions.assertEquals(9, QueryResults.asSet(tupleQuery.evaluate()).size()); + } + + // 2: bound (matching) object + try (RepositoryConnection con = fedxRepo.getConnection()) { + TupleQuery tupleQuery = con.prepareTupleQuery( + "PREFIX rdfs: " + + "SELECT * WHERE { " + + " ?subClass (rdfs:subClassOf*) ?myClass. " + + " } " + ); + tupleQuery.setBinding("myClass", Values.iri("http://example.org/C")); + + Assertions.assertEquals(3, QueryResults.asSet(tupleQuery.evaluate()).size()); + } + + // 2a: bound (matching) object + try (RepositoryConnection con = fedxRepo.getConnection()) { + TupleQuery tupleQuery = con.prepareTupleQuery( + "PREFIX rdfs: " + + "SELECT * WHERE { " + + " ?subClass (rdfs:subClassOf*) . " + + " } " + ); + + Assertions.assertEquals(3, QueryResults.asSet(tupleQuery.evaluate()).size()); + } + + // 3: bound (non-matching) object + try (RepositoryConnection con = fedxRepo.getConnection()) { + TupleQuery tupleQuery = con.prepareTupleQuery( + "PREFIX rdfs: " + + "PREFIX ex: " + + "SELECT * WHERE { " + + " ?subClass (ex:something*) ?myClass. " + + " } " + ); + tupleQuery.setBinding("myClass", Values.iri("http://example.org/C")); + + Assertions.assertEquals(1, QueryResults.asSet(tupleQuery.evaluate()).size()); + } + } + + @Test + public void testZeroLengthPath_length2_crossRepository() throws Exception { + + prepareTest(Arrays.asList("/tests/basic/data_emptyStore.ttl", "/tests/basic/data_emptyStore.ttl")); + + Repository repo1 = getRepository(1); + Repository repo2 = getRepository(2); + + try (RepositoryConnection con = repo1.getConnection()) { + con.add(Values.iri("http://example.org/A"), RDFS.SUBCLASSOF, Values.iri("http://example.org/B")); + } + + try (RepositoryConnection con = repo2.getConnection()) { + con.add(Values.iri("http://example.org/B"), RDFS.SUBCLASSOF, Values.iri("http://example.org/C")); + } + + Repository fedxRepo = fedxRule.getRepository(); + + // 2a: bound (matching) object + try (RepositoryConnection con = fedxRepo.getConnection()) { + TupleQuery tupleQuery = con.prepareTupleQuery( + "PREFIX rdfs: " + + "SELECT * WHERE { " + + " ?subClass (rdfs:subClassOf*) . " + + " } " + ); + + Assertions.assertEquals(3, QueryResults.asSet(tupleQuery.evaluate()).size()); + } + + } + + @Test + public void testPropertyPath_sourceSelection_crossRepository() throws Exception { + + prepareTest(Arrays.asList("/tests/basic/data_emptyStore.ttl", "/tests/basic/data_emptyStore.ttl")); + + Repository repo1 = getRepository(1); + Repository repo2 = getRepository(2); + + try (RepositoryConnection con = repo1.getConnection()) { + con.add(Values.iri("http://example.org/A"), RDFS.SUBCLASSOF, Values.iri("http://example.org/B"), + Values.iri("http://example.org/graph1")); + } + + try (RepositoryConnection con = repo2.getConnection()) { + con.add(Values.iri("http://example.org/B"), RDFS.SUBCLASSOF, Values.iri("http://example.org/C"), + Values.iri("http://example.org/graph2")); + } + + Repository fedxRepo = fedxRule.getRepository(); + + // 1a: bound (matching) object + try (RepositoryConnection con = fedxRepo.getConnection()) { + TupleQuery tupleQuery = con.prepareTupleQuery( + "PREFIX rdfs: " + + "SELECT * WHERE { " + + " ?subClass (rdfs:subClassOf+) . " + + " } " + ); + + Assertions.assertEquals(2, QueryResults.asSet(tupleQuery.evaluate()).size()); + } + + // 1b: with named graph + try (RepositoryConnection con = fedxRepo.getConnection()) { + TupleQuery tupleQuery = con.prepareTupleQuery( + "PREFIX rdfs: " + + "SELECT * WHERE { " + + " GRAPH {" + + " ?subClass (rdfs:subClassOf+) . " + + " }" + + "} " + ); + + Assertions.assertEquals(1, QueryResults.asSet(tupleQuery.evaluate()).size()); + } + + } + }