diff --git a/src/parser/sparqlParser/SparqlQleverVisitor.cpp b/src/parser/sparqlParser/SparqlQleverVisitor.cpp index 088f463ec5..f3d051145d 100644 --- a/src/parser/sparqlParser/SparqlQleverVisitor.cpp +++ b/src/parser/sparqlParser/SparqlQleverVisitor.cpp @@ -278,6 +278,47 @@ Alias Visitor::visit(Parser::AliasWithoutBracketsContext* ctx) { return {visitExpressionPimpl(ctx->expression()), visit(ctx->var())}; } +// ____________________________________________________________________________________ +parsedQuery::BasicGraphPattern Visitor::toGraphPattern( + const ad_utility::sparql_types::Triples& triples) { + parsedQuery::BasicGraphPattern pattern{}; + pattern._triples.reserve(triples.size()); + auto toTripleComponent = [](const T& item) { + namespace tc = ad_utility::triple_component; + if constexpr (ad_utility::isSimilar) { + return TripleComponent{item}; + } else if constexpr (ad_utility::isSimilar) { + // Blank Nodes in the pattern are to be treated as internal variables + // inside WHERE. + return TripleComponent{ + ParsedQuery::blankNodeToInternalVariable(item.toSparql())}; + } else { + static_assert(ad_utility::SimilarToAny); + return RdfStringParser>::parseTripleObject( + item.toSparql()); + } + }; + auto toPropertyPath = [](const T& item) -> PropertyPath { + if constexpr (ad_utility::isSimilar) { + return PropertyPath::fromVariable(item); + } else if constexpr (ad_utility::isSimilar) { + return PropertyPath::fromIri(item.toSparql()); + } else { + static_assert(ad_utility::SimilarToAny); + // This case can only happen if there's a bug in the SPARQL parser. + AD_THROW("Literals or blank nodes are not valid predicates."); + } + }; + for (const auto& triple : triples) { + auto subject = std::visit(toTripleComponent, triple.at(0)); + auto predicate = std::visit(toPropertyPath, triple.at(1)); + auto object = std::visit(toTripleComponent, triple.at(2)); + pattern._triples.emplace_back(std::move(subject), std::move(predicate), + std::move(object)); + } + return pattern; +} + // ____________________________________________________________________________________ ParsedQuery Visitor::visit(Parser::ConstructQueryContext* ctx) { ParsedQuery query; @@ -288,8 +329,16 @@ ParsedQuery Visitor::visit(Parser::ConstructQueryContext* ctx) { .value_or(parsedQuery::ConstructClause{}); visitWhereClause(ctx->whereClause(), query); } else { + // For `CONSTRUCT WHERE`, the CONSTRUCT template and the WHERE clause are + // syntactically the same, so we set the flag to true to keep the blank + // nodes, and convert them into variables during `toGraphPattern`. + isInsideConstructTriples_ = true; + auto cleanup = + absl::Cleanup{[this]() { isInsideConstructTriples_ = false; }}; query._clause = parsedQuery::ConstructClause{ visitOptional(ctx->triplesTemplate()).value_or(Triples{})}; + query._rootGraphPattern._graphPatterns.emplace_back( + toGraphPattern(query.constructClause().triples_)); } query.addSolutionModifiers(visit(ctx->solutionModifier())); diff --git a/src/parser/sparqlParser/SparqlQleverVisitor.h b/src/parser/sparqlParser/SparqlQleverVisitor.h index fb1cb9c05c..b3d629145a 100644 --- a/src/parser/sparqlParser/SparqlQleverVisitor.h +++ b/src/parser/sparqlParser/SparqlQleverVisitor.h @@ -7,6 +7,7 @@ #pragma once #include +#include #include "engine/sparqlExpressions/AggregateExpression.h" #include "engine/sparqlExpressions/NaryExpression.h" @@ -598,4 +599,14 @@ class SparqlQleverVisitor { void warnOrThrowIfUnboundVariables(auto* ctx, const SparqlExpressionPimpl& expression, std::string_view clauseName); + + // Convert an instance of `Triples` to a `BasicGraphPattern` so it can be used + // just like a WHERE clause. Most of the time this just changes the type and + // stays semantically the same, but for blank nodes, this step converts them + // into internal variables so they are interpreted correctly by the query + // planner. + static parsedQuery::BasicGraphPattern toGraphPattern( + const ad_utility::sparql_types::Triples& triples); + + FRIEND_TEST(SparqlParser, ensureExceptionOnInvalidGraphTerm); }; diff --git a/test/SparqlAntlrParserTest.cpp b/test/SparqlAntlrParserTest.cpp index a0e10d9b6f..cca4ebeddb 100644 --- a/test/SparqlAntlrParserTest.cpp +++ b/test/SparqlAntlrParserTest.cpp @@ -1215,9 +1215,30 @@ TEST(SparqlParser, ConstructQuery) { m::pq::LimitOffset({10}), m::pq::OrderKeys({{Var{"?a"}, false}}))); // This case of the grammar is not useful without Datasets, but we still // support it. - expectConstructQuery("CONSTRUCT WHERE { ?a ?b }", - m::ConstructQuery({{Var{"?a"}, Iri{""}, Var{"?b"}}}, - m::GraphPattern())); + expectConstructQuery( + "CONSTRUCT WHERE { ?a ?b }", + m::ConstructQuery( + {{Var{"?a"}, Iri{""}, Var{"?b"}}}, + m::GraphPattern(m::Triples({{Var{"?a"}, "", Var{"?b"}}})))); + + // Blank nodes turn into variables inside WHERE. + expectConstructQuery( + "CONSTRUCT WHERE { [] ?b }", + m::ConstructQuery( + {{BlankNode{true, "0"}, Iri{""}, Var{"?b"}}}, + m::GraphPattern(m::Triples( + {{Var{absl::StrCat(QLEVER_INTERNAL_BLANKNODE_VARIABLE_PREFIX, + "g_0")}, + "", Var{"?b"}}})))); + + // Test another variant to cover all cases. + expectConstructQuery( + "CONSTRUCT WHERE { ?foo \"Abc\"@en }", + m::ConstructQuery( + {{Iri{""}, Var{"?foo"}, Literal{"\"Abc\"@en"}}}, + m::GraphPattern(m::Triples( + {{iri(""), PropertyPath::fromVariable(Var{"?foo"}), + lit("\"Abc\"", "@en")}})))); // CONSTRUCT with datasets. expectConstructQuery( "CONSTRUCT { } FROM FROM NAMED FROM NAMED WHERE { }", @@ -1225,6 +1246,16 @@ TEST(SparqlParser, ConstructQuery) { m::Graphs{iri(""), iri("")})); } +// _____________________________________________________________________________ +TEST(SparqlParser, ensureExceptionOnInvalidGraphTerm) { + EXPECT_THROW(SparqlQleverVisitor::toGraphPattern( + {{Var{"?a"}, BlankNode{true, "0"}, Var{"?b"}}}), + ad_utility::Exception); + EXPECT_THROW(SparqlQleverVisitor::toGraphPattern( + {{Var{"?a"}, Literal{"\"Abc\""}, Var{"?b"}}}), + ad_utility::Exception); +} + // Test that ASK queries are parsed as they should. TEST(SparqlParser, AskQuery) { // Some helper functions and abbreviations.