diff --git a/core/queryalgebra/model/src/main/java/org/eclipse/rdf4j/query/algebra/ProjectionElemList.java b/core/queryalgebra/model/src/main/java/org/eclipse/rdf4j/query/algebra/ProjectionElemList.java index 53a4394f813..bef356caf80 100644 --- a/core/queryalgebra/model/src/main/java/org/eclipse/rdf4j/query/algebra/ProjectionElemList.java +++ b/core/queryalgebra/model/src/main/java/org/eclipse/rdf4j/query/algebra/ProjectionElemList.java @@ -11,6 +11,7 @@ package org.eclipse.rdf4j.query.algebra; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashSet; @@ -148,7 +149,7 @@ public ProjectionElemList clone() { clone.elements[i].setParentNode(clone); } - clone.elementsList = List.of(clone.elements); + clone.elementsList = Arrays.asList(clone.elements); return clone; } diff --git a/core/queryalgebra/model/src/main/java/org/eclipse/rdf4j/query/algebra/helpers/collectors/StatementPatternCollector.java b/core/queryalgebra/model/src/main/java/org/eclipse/rdf4j/query/algebra/helpers/collectors/StatementPatternCollector.java index 5dac23d105e..9cdc5cf232e 100644 --- a/core/queryalgebra/model/src/main/java/org/eclipse/rdf4j/query/algebra/helpers/collectors/StatementPatternCollector.java +++ b/core/queryalgebra/model/src/main/java/org/eclipse/rdf4j/query/algebra/helpers/collectors/StatementPatternCollector.java @@ -11,7 +11,9 @@ package org.eclipse.rdf4j.query.algebra.helpers.collectors; +import java.util.ArrayDeque; import java.util.ArrayList; +import java.util.Deque; import java.util.List; import org.eclipse.rdf4j.query.algebra.Filter; @@ -50,33 +52,32 @@ public void meet(Filter node) { @Override public void meet(Join node) throws RuntimeException { - TupleExpr leftArg = node.getLeftArg(); - TupleExpr rightArg = node.getRightArg(); - - // INSERT clause is often a deeply nested join. Recursive approach may cause stack overflow. Attempt - // non-recursive (or at least less-recursive) approach first. - while (true) { - if (leftArg instanceof Join && !(rightArg instanceof Join)) { - rightArg.visit(this); + if (!(node.getLeftArg() instanceof Join || node.getRightArg() instanceof Join)) { + super.meet(node); + return; + } - Join join = (Join) leftArg; - leftArg = join.getLeftArg(); - rightArg = join.getRightArg(); + Deque stack = new ArrayDeque<>(); + TupleExpr current = node; - } else if (rightArg instanceof Join && !(leftArg instanceof Join)) { - leftArg.visit(this); + while (true) { + // Drill down the leftmost spine, pushing right branches onto the stack + while (current instanceof Join) { + Join join = (Join) current; + stack.push(join.getRightArg()); // defer right side + current = join.getLeftArg(); // continue with left side + } - Join join = (Join) rightArg; - leftArg = join.getLeftArg(); - rightArg = join.getRightArg(); + // current is a leaf (not a Join) + current.visit(this); - } else { - leftArg.visit(this); - rightArg.visit(this); + // When the stack is empty, we have visited every deferred right branch + if (stack.isEmpty()) { return; } + // Pop the next right branch to process + current = stack.pop(); } - } @Override diff --git a/core/sail/memory/src/test/java/org/eclipse/rdf4j/sail/memory/QueryPlanRetrievalTest.java b/core/sail/memory/src/test/java/org/eclipse/rdf4j/sail/memory/QueryPlanRetrievalTest.java index d420c9b5d2e..d676190c81d 100644 --- a/core/sail/memory/src/test/java/org/eclipse/rdf4j/sail/memory/QueryPlanRetrievalTest.java +++ b/core/sail/memory/src/test/java/org/eclipse/rdf4j/sail/memory/QueryPlanRetrievalTest.java @@ -27,6 +27,7 @@ import org.eclipse.rdf4j.model.vocabulary.FOAF; import org.eclipse.rdf4j.model.vocabulary.RDF; import org.eclipse.rdf4j.model.vocabulary.RDFS; +import org.eclipse.rdf4j.query.GraphQuery; import org.eclipse.rdf4j.query.Query; import org.eclipse.rdf4j.query.TupleQuery; import org.eclipse.rdf4j.query.explanation.Explanation; @@ -101,6 +102,60 @@ public class QueryPlanRetrievalTest { " }\n" + "} GROUP BY ?countryID ?year"; + public static final String CONSTRUCT = "PREFIX epo: \n" + + "PREFIX rdf: \n" + + "PREFIX legal: \n" + + "PREFIX dcterms: \n" + + "PREFIX xsd: \n" + + "PREFIX dc: \n" + + "\n" + + "CONSTRUCT {" + + "\n" + + " ?proc a epo:Procedure .\n" + + " ?proc epo:hasProcedureType ?p .\n" + + " ?stat epo:concernsSubmissionsForLot ?lot .\n" + + " ?stat a epo:SubmissionStatisticalInformation .\n" + + " ?stat epo:hasReceivedTenders ?bidders .\n" + + " ?resultnotice a epo:ResultNotice .\n" + + " ?resultnotice epo:hasDispatchDate ?ddate .\n" + + " ?proc epo:hasProcurementScopeDividedIntoLot ?lot .\n" + + " ?resultnotice epo:refersToRole ?buyerrole .\n" + + " ?resultnotice epo:refersToProcedure ?proc .\n" + + "}" + + " WHERE {\n" + + + "\n" + + " ?proc a epo:Procedure .\n" + + " ?proc epo:hasProcedureType ?p .\n" + + " ?stat epo:concernsSubmissionsForLot ?lot .\n" + + " ?stat a epo:SubmissionStatisticalInformation .\n" + + " ?stat epo:hasReceivedTenders ?bidders .\n" + + " ?resultnotice a epo:ResultNotice .\n" + + " ?resultnotice epo:hasDispatchDate ?ddate .\n" + + " ?proc epo:hasProcurementScopeDividedIntoLot ?lot .\n" + + " ?resultnotice epo:refersToRole ?buyerrole .\n" + + " ?resultnotice epo:refersToProcedure ?proc .\n" + + "\n" + + " \tFILTER ( ?p != )\n" + + + "\t\tBIND(year(xsd:dateTime(?ddate)) AS ?year) .\n" + + "\n" + + "\n" + + " {\n" + + " SELECT DISTINCT ?buyerrole ?countryID WHERE {\n" + + " ?org epo:hasBuyerType ?buytype .\n" + + " FILTER (?buytype != )\n" + + + "\n" + + " ?buyerrole epo:playedBy ?org .\n" + + " ?org legal:registeredAddress ?orgaddress .\n" + + " ?orgaddress epo:hasCountryCode ?countrycode .\n" + + " ?countrycode dc:identifier ?countryID .\n" + + "\n" + + " }\n" + + " }\n" + + "} GROUP BY ?countryID ?year"; + public static final String UNION_QUERY = "select ?a where {?a a ?type. {?a ?b ?c, ?c2. {?c2 a ?type1}UNION{?c2 a ?type2}} UNION {?type ?d ?c}}"; ValueFactory vf = SimpleValueFactory.getInstance(); @@ -1534,6 +1589,211 @@ public void testArbitraryLengthPath() { } + @Test + public void constructQueryTest() { + + String expected = "Reduced\n" + + " MultiProjection\n" + + " ProjectionElemList\n" + + " ProjectionElem \"proc\" AS \"subject\"\n" + + " ProjectionElem \"_const_f5e5585a_uri\" AS \"predicate\"\n" + + " ProjectionElem \"_const_be18ee7b_uri\" AS \"object\"\n" + + " ProjectionElemList\n" + + " ProjectionElem \"proc\" AS \"subject\"\n" + + " ProjectionElem \"_const_9c756f6b_uri\" AS \"predicate\"\n" + + " ProjectionElem \"p\" AS \"object\"\n" + + " ProjectionElemList\n" + + " ProjectionElem \"stat\" AS \"subject\"\n" + + " ProjectionElem \"_const_25686184_uri\" AS \"predicate\"\n" + + " ProjectionElem \"lot\" AS \"object\"\n" + + " ProjectionElemList\n" + + " ProjectionElem \"stat\" AS \"subject\"\n" + + " ProjectionElem \"_const_f5e5585a_uri\" AS \"predicate\"\n" + + " ProjectionElem \"_const_ea79e75_uri\" AS \"object\"\n" + + " ProjectionElemList\n" + + " ProjectionElem \"stat\" AS \"subject\"\n" + + " ProjectionElem \"_const_98c73a3c_uri\" AS \"predicate\"\n" + + " ProjectionElem \"bidders\" AS \"object\"\n" + + " ProjectionElemList\n" + + " ProjectionElem \"resultnotice\" AS \"subject\"\n" + + " ProjectionElem \"_const_f5e5585a_uri\" AS \"predicate\"\n" + + " ProjectionElem \"_const_77e914ad_uri\" AS \"object\"\n" + + " ProjectionElemList\n" + + " ProjectionElem \"resultnotice\" AS \"subject\"\n" + + " ProjectionElem \"_const_1b0b00ca_uri\" AS \"predicate\"\n" + + " ProjectionElem \"ddate\" AS \"object\"\n" + + " ProjectionElemList\n" + + " ProjectionElem \"proc\" AS \"subject\"\n" + + " ProjectionElem \"_const_9c3f1eec_uri\" AS \"predicate\"\n" + + " ProjectionElem \"lot\" AS \"object\"\n" + + " ProjectionElemList\n" + + " ProjectionElem \"resultnotice\" AS \"subject\"\n" + + " ProjectionElem \"_const_6aa9a9c_uri\" AS \"predicate\"\n" + + " ProjectionElem \"buyerrole\" AS \"object\"\n" + + " ProjectionElemList\n" + + " ProjectionElem \"resultnotice\" AS \"subject\"\n" + + " ProjectionElem \"_const_183bd06d_uri\" AS \"predicate\"\n" + + " ProjectionElem \"proc\" AS \"object\"\n" + + " Extension\n" + + " Group (countryID, year)\n" + + " Join (HashJoinIteration)\n" + + " ╠══ Extension [left]\n" + + " ║ ├── Join (JoinIterator)\n" + + " ║ │ ╠══ StatementPattern (costEstimate=0.71, resultSizeEstimate=0) [left]\n" + + " ║ │ ║ s: Var (name=resultnotice)\n" + + " ║ │ ║ p: Var (name=_const_183bd06d_uri, value=http://data.europa.eu/a4g/ontology#refersToProcedure, anonymous)\n" + + + " ║ │ ║ o: Var (name=proc)\n" + + " ║ │ ╚══ Join (JoinIterator) [right]\n" + + " ║ │ ├── StatementPattern (costEstimate=1.00, resultSizeEstimate=0) [left]\n" + + " ║ │ │ s: Var (name=proc)\n" + + " ║ │ │ p: Var (name=_const_f5e5585a_uri, value=http://www.w3.org/1999/02/22-rdf-syntax-ns#type, anonymous)\n" + + + " ║ │ │ o: Var (name=_const_be18ee7b_uri, value=http://data.europa.eu/a4g/ontology#Procedure, anonymous)\n" + + + " ║ │ └── Join (JoinIterator) [right]\n" + + " ║ │ ╠══ StatementPattern (costEstimate=1.00, resultSizeEstimate=0) [left]\n" + + " ║ │ ║ s: Var (name=resultnotice)\n" + + " ║ │ ║ p: Var (name=_const_f5e5585a_uri, value=http://www.w3.org/1999/02/22-rdf-syntax-ns#type, anonymous)\n" + + + " ║ │ ║ o: Var (name=_const_77e914ad_uri, value=http://data.europa.eu/a4g/ontology#ResultNotice, anonymous)\n" + + + " ║ │ ╚══ Join (JoinIterator) [right]\n" + + " ║ │ ├── StatementPattern (costEstimate=1.12, resultSizeEstimate=0) [left]\n" + + " ║ │ │ s: Var (name=proc)\n" + + " ║ │ │ p: Var (name=_const_9c3f1eec_uri, value=http://data.europa.eu/a4g/ontology#hasProcurementScopeDividedIntoLot, anonymous)\n" + + + " ║ │ │ o: Var (name=lot)\n" + + " ║ │ └── Join (JoinIterator) [right]\n" + + " ║ │ ╠══ StatementPattern (costEstimate=0.75, resultSizeEstimate=0) [left]\n" + + + " ║ │ ║ s: Var (name=stat)\n" + + " ║ │ ║ p: Var (name=_const_25686184_uri, value=http://data.europa.eu/a4g/ontology#concernsSubmissionsForLot, anonymous)\n" + + + " ║ │ ║ o: Var (name=lot)\n" + + " ║ │ ╚══ Join (JoinIterator) [right]\n" + + " ║ │ ├── StatementPattern (costEstimate=1.00, resultSizeEstimate=0) [left]\n" + + + " ║ │ │ s: Var (name=stat)\n" + + " ║ │ │ p: Var (name=_const_f5e5585a_uri, value=http://www.w3.org/1999/02/22-rdf-syntax-ns#type, anonymous)\n" + + + " ║ │ │ o: Var (name=_const_ea79e75_uri, value=http://data.europa.eu/a4g/ontology#SubmissionStatisticalInformation, anonymous)\n" + + + " ║ │ └── Join (JoinIterator) [right]\n" + + " ║ │ ╠══ Filter [left]\n" + + " ║ │ ║ ├── Compare (!=)\n" + + " ║ │ ║ │ Var (name=p)\n" + + " ║ │ ║ │ ValueConstant (value=http://publications.europa.eu/resource/authority/procurement-procedure-type/neg-wo-call)\n" + + + " ║ │ ║ └── StatementPattern (costEstimate=2.24, resultSizeEstimate=0)\n" + + + " ║ │ ║ s: Var (name=proc)\n" + + " ║ │ ║ p: Var (name=_const_9c756f6b_uri, value=http://data.europa.eu/a4g/ontology#hasProcedureType, anonymous)\n" + + + " ║ │ ║ o: Var (name=p)\n" + + " ║ │ ╚══ Join (JoinIterator) [right]\n" + + " ║ │ ├── StatementPattern (costEstimate=2.24, resultSizeEstimate=0) [left]\n" + + + " ║ │ │ s: Var (name=stat)\n" + + " ║ │ │ p: Var (name=_const_98c73a3c_uri, value=http://data.europa.eu/a4g/ontology#hasReceivedTenders, anonymous)\n" + + + " ║ │ │ o: Var (name=bidders)\n" + + " ║ │ └── Join (JoinIterator) [right]\n" + + " ║ │ ╠══ StatementPattern (costEstimate=2.24, resultSizeEstimate=0) [left]\n" + + + " ║ │ ║ s: Var (name=resultnotice)\n" + + " ║ │ ║ p: Var (name=_const_1b0b00ca_uri, value=http://data.europa.eu/a4g/ontology#hasDispatchDate, anonymous)\n" + + + " ║ │ ║ o: Var (name=ddate)\n" + + " ║ │ ╚══ StatementPattern (costEstimate=2.24, resultSizeEstimate=0) [right]\n" + + + " ║ │ s: Var (name=resultnotice)\n" + + " ║ │ p: Var (name=_const_6aa9a9c_uri, value=http://data.europa.eu/a4g/ontology#refersToRole, anonymous)\n" + + + " ║ │ o: Var (name=buyerrole)\n" + + " ║ └── ExtensionElem (year)\n" + + " ║ FunctionCall (http://www.w3.org/2005/xpath-functions#year-from-dateTime)\n" + + " ║ FunctionCall (http://www.w3.org/2001/XMLSchema#dateTime)\n" + + " ║ Var (name=ddate)\n" + + " ╚══ Distinct (new scope) [right]\n" + + " Projection\n" + + " ╠══ ProjectionElemList\n" + + " ║ ProjectionElem \"buyerrole\"\n" + + " ║ ProjectionElem \"countryID\"\n" + + " ╚══ Join (JoinIterator)\n" + + " ├── StatementPattern (costEstimate=1.25, resultSizeEstimate=0) [left]\n" + + " │ s: Var (name=org)\n" + + " │ p: Var (name=_const_beb18915_uri, value=https://www.w3.org/ns/legal#registeredAddress, anonymous)\n" + + + " │ o: Var (name=orgaddress)\n" + + " └── Join (JoinIterator) [right]\n" + + " ╠══ StatementPattern (costEstimate=1.12, resultSizeEstimate=0) [left]\n" + + " ║ s: Var (name=orgaddress)\n" + + " ║ p: Var (name=_const_2f7de0e1_uri, value=http://data.europa.eu/a4g/ontology#hasCountryCode, anonymous)\n" + + + " ║ o: Var (name=countrycode)\n" + + " ╚══ Join (JoinIterator) [right]\n" + + " ├── Filter [left]\n" + + " │ ╠══ Compare (!=)\n" + + " │ ║ Var (name=buytype)\n" + + " │ ║ ValueConstant (value=http://publications.europa.eu/resource/authority/buyer-legal-type/eu-int-org)\n" + + + " │ ╚══ StatementPattern (costEstimate=2.24, resultSizeEstimate=0)\n" + + " │ s: Var (name=org)\n" + + " │ p: Var (name=_const_1abd8d4b_uri, value=http://data.europa.eu/a4g/ontology#hasBuyerType, anonymous)\n" + + + " │ o: Var (name=buytype)\n" + + " └── Join (JoinIterator) [right]\n" + + " ╠══ StatementPattern (costEstimate=2.24, resultSizeEstimate=0) [left]\n" + + + " ║ s: Var (name=buyerrole)\n" + + " ║ p: Var (name=_const_beb855c2_uri, value=http://data.europa.eu/a4g/ontology#playedBy, anonymous)\n" + + + " ║ o: Var (name=org)\n" + + " ╚══ StatementPattern (costEstimate=2.24, resultSizeEstimate=0) [right]\n" + + + " s: Var (name=countrycode)\n" + + " p: Var (name=_const_a825a5f4_uri, value=http://purl.org/dc/elements/1.1/identifier, anonymous)\n" + + + " o: Var (name=countryID)\n" + + " ExtensionElem (_const_f5e5585a_uri)\n" + + " ValueConstant (value=http://www.w3.org/1999/02/22-rdf-syntax-ns#type)\n" + + " ExtensionElem (_const_be18ee7b_uri)\n" + + " ValueConstant (value=http://data.europa.eu/a4g/ontology#Procedure)\n" + + " ExtensionElem (_const_9c756f6b_uri)\n" + + " ValueConstant (value=http://data.europa.eu/a4g/ontology#hasProcedureType)\n" + + " ExtensionElem (_const_25686184_uri)\n" + + " ValueConstant (value=http://data.europa.eu/a4g/ontology#concernsSubmissionsForLot)\n" + + " ExtensionElem (_const_ea79e75_uri)\n" + + " ValueConstant (value=http://data.europa.eu/a4g/ontology#SubmissionStatisticalInformation)\n" + + + " ExtensionElem (_const_98c73a3c_uri)\n" + + " ValueConstant (value=http://data.europa.eu/a4g/ontology#hasReceivedTenders)\n" + + " ExtensionElem (_const_77e914ad_uri)\n" + + " ValueConstant (value=http://data.europa.eu/a4g/ontology#ResultNotice)\n" + + " ExtensionElem (_const_1b0b00ca_uri)\n" + + " ValueConstant (value=http://data.europa.eu/a4g/ontology#hasDispatchDate)\n" + + " ExtensionElem (_const_9c3f1eec_uri)\n" + + " ValueConstant (value=http://data.europa.eu/a4g/ontology#hasProcurementScopeDividedIntoLot)\n" + + + " ExtensionElem (_const_6aa9a9c_uri)\n" + + " ValueConstant (value=http://data.europa.eu/a4g/ontology#refersToRole)\n" + + " ExtensionElem (_const_183bd06d_uri)\n" + + " ValueConstant (value=http://data.europa.eu/a4g/ontology#refersToProcedure)\n"; + SailRepository sailRepository = new SailRepository(new MemoryStore()); + addData(sailRepository); + + try (SailRepositoryConnection connection = sailRepository.getConnection()) { + GraphQuery query = connection.prepareGraphQuery(CONSTRUCT); + String actual = query.explain(Explanation.Level.Optimized).toString(); + + assertThat(actual).isEqualToNormalizingNewlines(expected); + + } + sailRepository.shutDown(); + + } + @Test public void testHaving() {