Skip to content

Commit

Permalink
Support empty group graph patterns (#1308)
Browse files Browse the repository at this point in the history
Queries like `SELECT * WHERE { }` or subqueries like `{ }` now work as they should (returning a single empty binding, which is the neutral element for the join operation). They don't appear very often in real life but frequently occur in the SPARQL conformance tests.
  • Loading branch information
joka921 authored Mar 21, 2024
1 parent 212e14a commit bb9959a
Show file tree
Hide file tree
Showing 6 changed files with 39 additions and 36 deletions.
15 changes: 3 additions & 12 deletions src/engine/ExportQueryExecutionTrees.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,9 @@ nlohmann::json ExportQueryExecutionTrees::idTableToQLeverJSONArray(
nlohmann::json json = nlohmann::json::array();

for (size_t rowIndex : getRowIndices(limitAndOffset, data)) {
json.emplace_back();
// We need the explicit `array` constructor for the special case of zero
// variables.
json.push_back(nlohmann::json::array());
auto& row = json.back();
for (const auto& opt : columns) {
if (!opt) {
Expand Down Expand Up @@ -390,12 +392,6 @@ nlohmann::json ExportQueryExecutionTrees::selectQueryResultBindingsToQLeverJSON(
QueryExecutionTree::ColumnIndicesAndTypes selectedColumnIndices =
qet.selectedVariablesToColumnIndices(selectClause, true);

// This can never happen, because empty SELECT clauses are not supported by
// QLever. Should we ever support triples without variables then this might
// theoretically happen in combination with `SELECT *`, but then this still
// can be changed.
AD_CORRECTNESS_CHECK(!selectedColumnIndices.empty());

return ExportQueryExecutionTrees::idTableToQLeverJSONArray(
qet, limitAndOffset, selectedColumnIndices, std::move(resultTable),
std::move(cancellationHandle));
Expand Down Expand Up @@ -425,11 +421,6 @@ ExportQueryExecutionTrees::selectQueryResultToStream(
<< std::endl;
auto selectedColumnIndices =
qet.selectedVariablesToColumnIndices(selectClause, true);
// This case should only fail if we have no variables selected at all.
// This case should be handled earlier by the parser.
// TODO<joka921, hannahbast> What do we want to do for variables that don't
// appear in the query body?
AD_CONTRACT_CHECK(!selectedColumnIndices.empty());

const auto& idTable = resultTable->idTable();
// special case : binary export of IdTable
Expand Down
19 changes: 8 additions & 11 deletions src/engine/QueryPlanner.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,10 @@ QueryExecutionTree QueryPlanner::createExecutionTree(ParsedQuery& pq) {

std::vector<QueryPlanner::SubtreePlan> QueryPlanner::optimize(
ParsedQuery::GraphPattern* rootPattern) {
// Handle the empty pattern
if (rootPattern->_graphPatterns.empty()) {
return {makeSubtreePlan<NeutralElementOperation>(_qec)};
}
// here we collect a set of possible plans for each of our children.
// always only holds plans for children that can be joined in an
// arbitrary order
Expand Down Expand Up @@ -215,11 +219,6 @@ std::vector<QueryPlanner::SubtreePlan> QueryPlanner::optimize(
// find a single best candidate for a given graph pattern
auto optimizeSingle = [this](const auto pattern) -> SubtreePlan {
auto v = optimize(pattern);
if (v.empty()) {
throw std::runtime_error(
"grandchildren or lower of a Plan to be optimized may never be "
"empty");
}
auto idx = findCheapestExecutionTree(v);
return std::move(v[idx]);
};
Expand Down Expand Up @@ -279,11 +278,8 @@ std::vector<QueryPlanner::SubtreePlan> QueryPlanner::optimize(
} else {
static_assert(
std::is_same_v<std::vector<SubtreePlan>, std::decay_t<decltype(v)>>);
if (v.empty()) {
throw std::runtime_error(
"grandchildren or lower of a Plan to be optimized may never be "
"empty. Please report this");
}
// Empty group graph patterns should have been handled previously.
AD_CORRECTNESS_CHECK(!v.empty());

// optionals that occur before any of their variables have been bound
// actually behave like ordinary (Group)GraphPatterns
Expand Down Expand Up @@ -382,14 +378,15 @@ std::vector<QueryPlanner::SubtreePlan> QueryPlanner::optimize(
makeSubtreePlan<Union>(_qec, left._qet, right._qet);
joinCandidates(std::vector{std::move(candidate)});
} else if constexpr (std::is_same_v<T, p::Subquery>) {
ParsedQuery& subquery = arg.get();
// TODO<joka921> We currently do not optimize across subquery borders
// but abuse them as "optimization hints". In theory, one could even
// remove the ORDER BY clauses of a subquery if we can prove that
// the results will be reordered anyway.

// For a subquery, make sure that one optimal result for each ordering
// of the result (by a single column) is contained.
auto candidatesForSubquery = createExecutionTrees(arg.get());
auto candidatesForSubquery = createExecutionTrees(subquery);
// Make sure that variables that are not selected by the subquery are
// not visible.
auto setSelectedVariables = [&](SubtreePlan& plan) {
Expand Down
7 changes: 0 additions & 7 deletions src/parser/sparqlParser/SparqlQleverVisitor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -336,13 +336,6 @@ GraphPattern Visitor::visit(Parser::GroupGraphPatternContext* ctx) {
} else {
AD_CORRECTNESS_CHECK(ctx->groupGraphPatternSub());
auto [subOps, filters] = visit(ctx->groupGraphPatternSub());

if (subOps.empty()) {
reportError(ctx,
"QLever currently doesn't support empty GroupGraphPatterns "
"and WHERE clauses");
}

pattern._graphPatterns = std::move(subOps);
for (auto& filter : filters) {
if (auto langFilterData =
Expand Down
11 changes: 11 additions & 0 deletions test/QueryPlannerTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -897,6 +897,17 @@ TEST(QueryPlanner, NonDistinctVariablesInTriple) {
h::IndexScanFromStrings(internalVar(0), "?s", "<o>")));
}

TEST(QueryPlanner, emptyGroupGraphPattern) {
h::expect("SELECT * WHERE {}", h::NeutralElement());
h::expect("SELECT * WHERE { {} }", h::NeutralElement());
h::expect("SELECT * WHERE { {} {} }",
h::CartesianProductJoin(h::NeutralElement(), h::NeutralElement()));
h::expect("SELECT * WHERE { {} UNION {} }",
h::Union(h::NeutralElement(), h::NeutralElement()));
h::expect("SELECT * WHERE { {} { SELECT * WHERE {}}}",
h::CartesianProductJoin(h::NeutralElement(), h::NeutralElement()));
}

// __________________________________________________________________________
TEST(QueryPlanner, TooManyTriples) {
std::string query = "SELECT * WHERE {";
Expand Down
15 changes: 12 additions & 3 deletions test/QueryPlannerTestHelpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#include "engine/TextIndexScanForEntity.h"
#include "engine/TextIndexScanForWord.h"
#include "engine/TransitivePath.h"
#include "engine/Union.h"
#include "gmock/gmock-matchers.h"
#include "gmock/gmock.h"
#include "parser/SparqlParser.h"
Expand Down Expand Up @@ -71,7 +72,7 @@ inline auto MatchTypeAndOrderedChildren =
/// single `IndexScan` with the given `subject`, `predicate`, and `object`, and
/// that the `ScanType` of this `IndexScan` is any of the given
/// `allowedPermutations`.
inline auto IndexScan =
constexpr auto IndexScan =
[](TripleComponent subject, TripleComponent predicate,
TripleComponent object,
const std::vector<Permutation::Enum>& allowedPermutations = {})
Expand All @@ -90,8 +91,13 @@ inline auto IndexScan =
AD_PROPERTY(IndexScan, getObject, Eq(object))));
};

inline auto TextIndexScanForWord = [](Variable textRecordVar,
string word) -> QetMatcher {
// Match the `NeutralElementOperation`.
constexpr auto NeutralElement = []() -> QetMatcher {
return MatchTypeAndOrderedChildren<::NeutralElementOperation>();
};

constexpr auto TextIndexScanForWord = [](Variable textRecordVar,
string word) -> QetMatcher {
return RootOperation<::TextIndexScanForWord>(AllOf(
AD_PROPERTY(::TextIndexScanForWord, getResultWidth,
Eq(1 + word.ends_with('*'))),
Expand Down Expand Up @@ -255,6 +261,9 @@ constexpr auto OrderBy = [](const ::OrderBy::SortedVariables& sortedVariables,
AD_PROPERTY(::OrderBy, getSortedVariables, Eq(sortedVariables))));
};

// Match a `UNION` operation.
constexpr auto Union = MatchTypeAndOrderedChildren<::Union>;

/// Parse the given SPARQL `query`, pass it to a `QueryPlanner` with empty
/// execution context, and return the resulting `QueryExecutionTree`
QueryExecutionTree parseAndPlan(std::string query, QueryExecutionContext* qec) {
Expand Down
8 changes: 5 additions & 3 deletions test/SparqlAntlrParserTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -867,9 +867,11 @@ TEST(SparqlParser, GroupGraphPattern) {
ExpectParseFails<&Parser::groupGraphPattern>{{}};
auto DummyTriplesMatcher = m::Triples({{Var{"?x"}, "?y", Var{"?z"}}});

// Empty GraphPatterns are not supported.
expectGroupGraphPatternFails("{ }");
expectGroupGraphPatternFails("{ SELECT * WHERE { } }");
// Empty GraphPatterns.
expectGraphPattern("{ }", m::GraphPattern());
expectGraphPattern(
"{ SELECT * WHERE { } }",
m::GraphPattern(m::SubSelect(::testing::_, m::GraphPattern())));

SparqlTriple abc{Var{"?a"}, "?b", Var{"?c"}};
SparqlTriple def{Var{"?d"}, "?e", Var{"?f"}};
Expand Down

0 comments on commit bb9959a

Please sign in to comment.