From 9ae7e170f7754ae542dee9bc0924e0cfcf1bc95c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5vard=20M=2E=20Ottestad?= Date: Tue, 7 Oct 2025 19:53:11 +0200 Subject: [PATCH] GH-3220 Fix property path object list handling --- .../query/parser/sparql/TupleExprBuilder.java | 41 +++++++++++++++++-- .../sparql/TestPropPathMisbehaviour.java | 32 +++++++++++++++ 2 files changed, 70 insertions(+), 3 deletions(-) diff --git a/core/queryparser/sparql/src/main/java/org/eclipse/rdf4j/query/parser/sparql/TupleExprBuilder.java b/core/queryparser/sparql/src/main/java/org/eclipse/rdf4j/query/parser/sparql/TupleExprBuilder.java index 0a4a50a5ea7..8da4687bbd6 100644 --- a/core/queryparser/sparql/src/main/java/org/eclipse/rdf4j/query/parser/sparql/TupleExprBuilder.java +++ b/core/queryparser/sparql/src/main/java/org/eclipse/rdf4j/query/parser/sparql/TupleExprBuilder.java @@ -1449,6 +1449,36 @@ public TupleExpr visit(ASTPathSequence pathSeqNode, Object data) throws VisitorE subjVar = mapValueExprToVar(data); } + List objectList = null; + if (!(data instanceof PathSequenceContext)) { + ASTObjectList objectListNode = getObjectList(pathSeqNode); + if (objectListNode != null) { + @SuppressWarnings("unchecked") + List evaluatedObjectList = (List) objectListNode.jjtAccept(this, null); + objectList = evaluatedObjectList; + + if (objectList.size() > 1) { + TupleExpr result = null; + for (ValueExpr objectItem : objectList) { + PathSequenceContext objectContext = new PathSequenceContext(); + objectContext.scope = graphPattern.getStatementPatternScope(); + objectContext.contextVar = graphPattern.getContextVar(); + objectContext.startVar = subjVar; + objectContext.endVar = mapValueExprToVar(objectItem); + + TupleExpr pathExpr = (TupleExpr) pathSeqNode.jjtAccept(this, objectContext); + + if (result == null) { + result = pathExpr; + } else { + result = new Join(result, pathExpr); + } + } + return result; + } + } + } + List pathElements = pathSeqNode.getPathElements(); int pathLength = pathElements.size(); @@ -1470,10 +1500,15 @@ public TupleExpr visit(ASTPathSequence pathSeqNode, Object data) throws VisitorE // We handle this here instead of higher up in the tree visitor because here we have // a reference to the "temporary" endVar that needs to be replaced. - @SuppressWarnings("unchecked") - List objectList = (List) getObjectList(pathSeqNode).jjtAccept(this, null); + List finalObjectList = objectList; + if (finalObjectList == null) { + @SuppressWarnings("unchecked") + List evaluatedObjectList = (List) getObjectList(pathSeqNode) + .jjtAccept(this, null); + finalObjectList = evaluatedObjectList; + } - for (ValueExpr objectItem : objectList) { + for (ValueExpr objectItem : finalObjectList) { Var objectVar = mapValueExprToVar(objectItem); Var replacement = objectVar; if (objectVar.equals(subjVar)) { // corner case for cyclic expressions, see SES-1685 diff --git a/core/queryparser/sparql/src/test/java/org/eclipse/rdf4j/query/parser/sparql/TestPropPathMisbehaviour.java b/core/queryparser/sparql/src/test/java/org/eclipse/rdf4j/query/parser/sparql/TestPropPathMisbehaviour.java index f208983655b..c38e867253c 100644 --- a/core/queryparser/sparql/src/test/java/org/eclipse/rdf4j/query/parser/sparql/TestPropPathMisbehaviour.java +++ b/core/queryparser/sparql/src/test/java/org/eclipse/rdf4j/query/parser/sparql/TestPropPathMisbehaviour.java @@ -10,9 +10,14 @@ *******************************************************************************/ package org.eclipse.rdf4j.query.parser.sparql; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + import org.eclipse.rdf4j.query.algebra.ArbitraryLengthPath; import org.eclipse.rdf4j.query.algebra.Distinct; import org.eclipse.rdf4j.query.algebra.Join; @@ -22,6 +27,7 @@ import org.eclipse.rdf4j.query.algebra.TupleExpr; import org.eclipse.rdf4j.query.algebra.Union; import org.eclipse.rdf4j.query.algebra.ZeroLengthPath; +import org.eclipse.rdf4j.query.algebra.helpers.collectors.StatementPatternCollector; import org.eclipse.rdf4j.query.parser.ParsedQuery; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -109,4 +115,30 @@ public void testGH3053() { "expect Union Right arg to be StatementPattern"); assertTrue(!proj2.isSubquery(), "expect projection to do NOT be a subQuery"); } + + @Test + public void testGH3220() { + String query = "select * where { ?i / ?v1, ?v2 . }"; + ParsedQuery parsedQuery = parser.parseQuery(query, "http://example.org/"); + + assertNotNull(parsedQuery); + + List statementPatterns = StatementPatternCollector + .process(parsedQuery.getTupleExpr()); + + List prop1Patterns = statementPatterns.stream() + .filter(sp -> sp.getPredicateVar().hasValue() + && "urn:prop1".equals(sp.getPredicateVar().getValue().stringValue())) + .collect(Collectors.toList()); + + assertEquals(2, prop1Patterns.size(), + "expected each object list entry to yield its own statement pattern for the first path element"); + + Set intermediateVarNames = prop1Patterns.stream() + .map(sp -> sp.getObjectVar().getName()) + .collect(Collectors.toSet()); + + assertEquals(2, intermediateVarNames.size(), + "expected unique intermediate variable per object list entry for the first path element"); + } }