Skip to content

Commit

Permalink
Implement Graph Store HTTP Protocol operations GET and POST (#1727)
Browse files Browse the repository at this point in the history
Integrate the Graph Store HTTP Protocol backend code from #1668. With this change, QLever now supports the graph management operations  `GET` (get all triples from specified graph) and `POST` (insert triples from payload into specified graph). Here are four example calls (the first two do a `GET`, the last two do a `POST`, the `-s` stands for silent, the `?default` stands for the default graph):

```
curl -Gs "localhost:7001/?graph=http://example.org/42 -H "Accept: text/turtle"
curl -Gs "localhost:7001/?default" -H "Accept: text/turtle"
curl -s "localhost:7001/?graph=http://example.org/42&access-token=bla" -H "Content-type: text/turtle" -d "<http://example.org> <http://example.org> 42"
curl -s "localhost:7001/?default&access-token=bla" -H "Content-type: text/turtle" -d "<http://example.org> <http://example.org> 42"
```
  • Loading branch information
Qup42 authored Feb 21, 2025
1 parent dffbc2b commit 8fe0642
Show file tree
Hide file tree
Showing 18 changed files with 1,359 additions and 670 deletions.
2 changes: 1 addition & 1 deletion src/engine/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,5 @@ add_library(engine
TextLimit.cpp LazyGroupBy.cpp GroupByHashMapOptimization.cpp SpatialJoin.cpp
CountConnectedSubgraphs.cpp SpatialJoinAlgorithms.cpp PathSearch.cpp ExecuteUpdate.cpp
Describe.cpp GraphStoreProtocol.cpp
QueryExecutionContext.cpp ExistsJoin.cpp)
QueryExecutionContext.cpp ExistsJoin.cpp SPARQLProtocol.cpp ParsedRequestBuilder.cpp)
qlever_target_link_libraries(engine util index parser sparqlExpressions http SortPerformanceEstimator Boost::iostreams s2)
40 changes: 9 additions & 31 deletions src/engine/GraphStoreProtocol.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,9 @@

#include "engine/GraphStoreProtocol.h"

#include "parser/SparqlParser.h"
#include "util/http/beast.h"

// ____________________________________________________________________________
GraphOrDefault GraphStoreProtocol::extractTargetGraph(
const ad_utility::url_parser::ParamValueMap& params) {
const std::optional<std::string> graphIri =
ad_utility::url_parser::checkParameter(params, "graph", std::nullopt);
const bool isDefault =
ad_utility::url_parser::checkParameter(params, "default", "").has_value();
if (graphIri.has_value() == isDefault) {
throw std::runtime_error(
"Exactly one of the query parameters default or graph must be set to "
"identify the graph for the graph store protocol request.");
}
if (graphIri.has_value()) {
return GraphRef::fromIrirefWithoutBrackets(graphIri.value());
} else {
AD_CORRECTNESS_CHECK(isDefault);
return DEFAULT{};
}
}

// ____________________________________________________________________________
void GraphStoreProtocol::throwUnsupportedMediatype(
const string_view& mediatype) {
Expand Down Expand Up @@ -84,18 +65,15 @@ std::vector<SparqlTripleSimpleWithGraph> GraphStoreProtocol::convertTriples(

// ____________________________________________________________________________
ParsedQuery GraphStoreProtocol::transformGet(const GraphOrDefault& graph) {
ParsedQuery res;
res._clause = parsedQuery::ConstructClause(
{{Variable("?s"), Variable("?p"), Variable("?o")}});
res._rootGraphPattern = {};
parsedQuery::GraphPattern selectSPO;
selectSPO._graphPatterns.emplace_back(parsedQuery::BasicGraphPattern{
{SparqlTriple(Variable("?s"), "?p", Variable("?o"))}});
// Construct the parsed query from its short equivalent SPARQL Update string.
// This is easier and also provides e.g. the `_originalString` field.
std::string query;
if (const auto* iri =
std::get_if<ad_utility::triple_component::Iri>(&graph)) {
res.datasetClauses_ =
parsedQuery::DatasetClauses::fromClauses({DatasetClause{*iri, false}});
query = absl::StrCat("CONSTRUCT { ?s ?p ?o } WHERE { GRAPH ",
iri->toStringRepresentation(), " { ?s ?p ?o } }");
} else {
query = "CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }";
}
res._rootGraphPattern = std::move(selectSPO);
return res;
return SparqlParser::parseQuery(query);
}
27 changes: 10 additions & 17 deletions src/engine/GraphStoreProtocol.h
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ class GraphStoreProtocol {
updateClause::GraphUpdate up{std::move(convertedTriples), {}};
ParsedQuery res;
res._clause = parsedQuery::UpdateClause{std::move(up)};
// Graph store protocol POST requests might have a very large body. Limit
// the length used for the string representation (arbitrarily) to 5000.
string_view body = string_view(rawRequest.body()).substr(0, 5000);
res._originalString = absl::StrCat("Graph Store POST Operation\n", body);
return res;
}
FRIEND_TEST(GraphStoreProtocolTest, transformPost);
Expand All @@ -101,25 +105,22 @@ class GraphStoreProtocol {
// Update.
CPP_template_2(typename RequestT)(
requires ad_utility::httpUtils::HttpRequest<RequestT>) static ParsedQuery
transformGraphStoreProtocol(const RequestT& rawRequest) {
transformGraphStoreProtocol(
ad_utility::url_parser::sparqlOperation::GraphStoreOperation
operation,
const RequestT& rawRequest) {
ad_utility::url_parser::ParsedUrl parsedUrl =
ad_utility::url_parser::parseRequestTarget(rawRequest.target());
// We only support passing the target graph as a query parameter (`Indirect
// Graph Identification`). `Direct Graph Identification` (the URL is the
// graph) is not supported. See also
// https://www.w3.org/TR/2013/REC-sparql11-http-rdf-update-20130321/#graph-identification.
GraphOrDefault graph = extractTargetGraph(parsedUrl.parameters_);

using enum boost::beast::http::verb;
auto method = rawRequest.method();
if (method == get) {
return transformGet(graph);
return transformGet(operation.graph_);
} else if (method == put) {
throwUnsupportedHTTPMethod("PUT");
} else if (method == delete_) {
throwUnsupportedHTTPMethod("DELETE");
} else if (method == post) {
return transformPost(rawRequest, graph);
return transformPost(rawRequest, operation.graph_);
} else if (method == head) {
throwUnsupportedHTTPMethod("HEAD");
} else if (method == patch) {
Expand All @@ -131,12 +132,4 @@ class GraphStoreProtocol {
"\" for the SPARQL Graph Store HTTP Protocol."));
}
}

private:
// Extract the graph to be acted upon using from the URL query parameters
// (`Indirect Graph Identification`). See
// https://www.w3.org/TR/2013/REC-sparql11-http-rdf-update-20130321/#indirect-graph-identification
static GraphOrDefault extractTargetGraph(
const ad_utility::url_parser::ParamValueMap& params);
FRIEND_TEST(GraphStoreProtocolTest, extractTargetGraph);
};
163 changes: 163 additions & 0 deletions src/engine/ParsedRequestBuilder.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
// Copyright 2025, University of Freiburg
// Chair of Algorithms and Data Structures
// Authors: Julian Mundhahs <[email protected]>

#include "ParsedRequestBuilder.h"

using namespace ad_utility::url_parser::sparqlOperation;

// ____________________________________________________________________________
ParsedRequestBuilder::ParsedRequestBuilder(const RequestType& request) {
using namespace ad_utility::url_parser::sparqlOperation;
// For an HTTP request, `request.target()` yields the HTTP Request-URI.
// This is a concatenation of the URL path and the query strings.
auto parsedUrl = ad_utility::url_parser::parseRequestTarget(request.target());
parsedRequest_ = {std::move(parsedUrl.path_), std::nullopt,
std::move(parsedUrl.parameters_), None{}};
}

// ____________________________________________________________________________
void ParsedRequestBuilder::extractAccessToken(const RequestType& request) {
parsedRequest_.accessToken_ =
determineAccessToken(request, parsedRequest_.parameters_);
}

// ____________________________________________________________________________
void ParsedRequestBuilder::extractDatasetClauses() {
extractDatasetClauseIfOperationIs<Query>("default-graph-uri", false);
extractDatasetClauseIfOperationIs<Query>("named-graph-uri", true);
extractDatasetClauseIfOperationIs<Update>("using-graph-uri", false);
extractDatasetClauseIfOperationIs<Update>("using-named-graph-uri", true);
}

// ____________________________________________________________________________
bool ParsedRequestBuilder::parameterIsContainedExactlyOnce(
std::string_view key) const {
return ad_utility::url_parser::getParameterCheckAtMostOnce(
parsedRequest_.parameters_, key)
.has_value();
}

// ____________________________________________________________________________
bool ParsedRequestBuilder::isGraphStoreOperation() const {
return parameterIsContainedExactlyOnce("graph") ||
parameterIsContainedExactlyOnce("default");
}

// ____________________________________________________________________________
void ParsedRequestBuilder::extractGraphStoreOperation() {
// SPARQL Graph Store HTTP Protocol with indirect graph identification
if (parameterIsContainedExactlyOnce("graph") &&
parameterIsContainedExactlyOnce("default")) {
throw std::runtime_error(
R"(Parameters "graph" and "default" must not be set at the same time.)");
}
AD_CORRECTNESS_CHECK(std::holds_alternative<None>(parsedRequest_.operation_));
// We only support passing the target graph as a query parameter
// (`Indirect Graph Identification`). `Direct Graph Identification` (the
// URL is the graph) is not supported. See also
// https://www.w3.org/TR/2013/REC-sparql11-http-rdf-update-20130321/#graph-identification.
parsedRequest_.operation_ =
GraphStoreOperation{extractTargetGraph(parsedRequest_.parameters_)};
}

// ____________________________________________________________________________
bool ParsedRequestBuilder::parametersContain(std::string_view param) const {
return parsedRequest_.parameters_.contains(param);
}

// ____________________________________________________________________________
ad_utility::url_parser::ParsedRequest ParsedRequestBuilder::build() && {
return std::move(parsedRequest_);
}

// ____________________________________________________________________________
void ParsedRequestBuilder::reportUnsupportedContentTypeIfGraphStore(
std::string_view contentType) const {
if (isGraphStoreOperation()) {
throw std::runtime_error(absl::StrCat("Unsupported Content type \"",
contentType,
"\" for Graph Store protocol."));
}
}

// ____________________________________________________________________________
template <typename Operation>
void ParsedRequestBuilder::extractDatasetClauseIfOperationIs(
const std::string& key, bool isNamed) {
if (Operation* op = std::get_if<Operation>(&parsedRequest_.operation_)) {
ad_utility::appendVector(op->datasetClauses_,
ad_utility::url_parser::parseDatasetClausesFrom(
parsedRequest_.parameters_, key, isNamed));
}
}

// ____________________________________________________________________________
template <typename Operation>
void ParsedRequestBuilder::extractOperationIfSpecified(string_view paramName) {
auto operation = ad_utility::url_parser::getParameterCheckAtMostOnce(
parsedRequest_.parameters_, paramName);
if (operation.has_value()) {
AD_CORRECTNESS_CHECK(
std::holds_alternative<None>(parsedRequest_.operation_));
parsedRequest_.operation_ = Operation{operation.value(), {}};
parsedRequest_.parameters_.erase(paramName);
}
}

template void ParsedRequestBuilder::extractOperationIfSpecified<Query>(
string_view paramName);
template void ParsedRequestBuilder::extractOperationIfSpecified<Update>(
string_view paramName);

// ____________________________________________________________________________
GraphOrDefault ParsedRequestBuilder::extractTargetGraph(
const ad_utility::url_parser::ParamValueMap& params) {
const std::optional<std::string> graphIri =
ad_utility::url_parser::checkParameter(params, "graph", std::nullopt);
const bool isDefault =
ad_utility::url_parser::checkParameter(params, "default", "").has_value();
if (graphIri.has_value() == isDefault) {
throw std::runtime_error(
R"(Exactly one of the query parameters "default" or "graph" must be set to identify the graph for the graph store protocol request.)");
}
if (graphIri.has_value()) {
return GraphRef::fromIrirefWithoutBrackets(graphIri.value());
} else {
AD_CORRECTNESS_CHECK(isDefault);
return DEFAULT{};
}
}

// ____________________________________________________________________________
std::optional<std::string> ParsedRequestBuilder::determineAccessToken(
const RequestType& request,
const ad_utility::url_parser::ParamValueMap& params) {
namespace http = boost::beast::http;
std::optional<std::string> tokenFromAuthorizationHeader;
std::optional<std::string> tokenFromParameter;
if (request.find(http::field::authorization) != request.end()) {
string_view authorization = request[http::field::authorization];
const std::string prefix = "Bearer ";
if (!authorization.starts_with(prefix)) {
throw std::runtime_error(absl::StrCat(
"Authorization header doesn't start with \"", prefix, "\"."));
}
authorization.remove_prefix(prefix.length());
tokenFromAuthorizationHeader = std::string(authorization);
}
if (params.contains("access-token")) {
tokenFromParameter = ad_utility::url_parser::getParameterCheckAtMostOnce(
params, "access-token");
}
// If both are specified, they must be equal. This way there is no hidden
// precedence.
if (tokenFromAuthorizationHeader && tokenFromParameter &&
tokenFromAuthorizationHeader != tokenFromParameter) {
throw std::runtime_error(
"Access token is specified both in the `Authorization` header and by "
"the `access-token` parameter, but they are not the same");
}
return tokenFromAuthorizationHeader ? std::move(tokenFromAuthorizationHeader)
: std::move(tokenFromParameter);
}
83 changes: 83 additions & 0 deletions src/engine/ParsedRequestBuilder.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Copyright 2025, University of Freiburg
// Chair of Algorithms and Data Structures
// Authors: Julian Mundhahs <[email protected]>

#pragma once

#include "util/http/UrlParser.h"
#include "util/http/beast.h"

// Helper for parsing `HttpRequest` into `ParsedRequest`. The parsing has many
// common patterns but the details are slightly different. This struct
// stores the partially parsed `ParsedRequest` and methods for common
// operations used while parsing.
struct ParsedRequestBuilder {
FRIEND_TEST(ParsedRequestBuilderTest, extractTargetGraph);
FRIEND_TEST(ParsedRequestBuilderTest, determineAccessToken);
FRIEND_TEST(ParsedRequestBuilderTest, parameterIsContainedExactlyOnce);

using RequestType =
boost::beast::http::request<boost::beast::http::string_body>;

ad_utility::url_parser::ParsedRequest parsedRequest_;

// Initialize a `ParsedRequestBuilder`, parsing the request target into the
// `ParsedRequest`.
explicit ParsedRequestBuilder(const RequestType& request);

// Extract the access token from the access-token parameter or the
// Authorization header and set it for `ParsedRequest`. If both are given,
// then they must be the same.
void extractAccessToken(const RequestType& request);

// If applicable extract the dataset clauses from the parameters and set them
// on the Query or Update.
void extractDatasetClauses();

// If the parameter is set, set the operation with the parameter's value as
// operation string and empty dataset clauses. Setting an operation when one
// is already set is an error. Note: processed parameters are removed from the
// parameter map.
template <typename Operation>
void extractOperationIfSpecified(string_view paramName);

// Returns whether the request is a Graph Store operation.
bool isGraphStoreOperation() const;

// Set the operation to the parsed Graph Store operation.
void extractGraphStoreOperation();

// Returns whether the parameters contain a parameter with the given key.
bool parametersContain(std::string_view param) const;

// Check that requests don't both have these content types and are Graph
// Store operations.
void reportUnsupportedContentTypeIfGraphStore(
std::string_view contentType) const;

// Move the `ParsedRequest` out when parsing is finished.
ad_utility::url_parser::ParsedRequest build() &&;

private:
// Adds a dataset clause to the operation if it is of the given type. The
// dataset clause's IRI is the value of parameter `key`. The `isNamed_` of the
// dataset clause is as given.
template <typename Operation>
void extractDatasetClauseIfOperationIs(const std::string& key, bool isNamed);

// Check that a parameter is contained exactly once. An exception is thrown if
// a parameter is contained more than once.
bool parameterIsContainedExactlyOnce(std::string_view key) const;

// Extract the graph to be acted upon using from the URL query parameters
// (`Indirect Graph Identification`). See
// https://www.w3.org/TR/2013/REC-sparql11-http-rdf-update-20130321/#indirect-graph-identification
static GraphOrDefault extractTargetGraph(
const ad_utility::url_parser::ParamValueMap& params);

// Determine the access token from the parameters and the requests
// Authorization header.
static std::optional<std::string> determineAccessToken(
const RequestType& request,
const ad_utility::url_parser::ParamValueMap& params);
};
Loading

0 comments on commit 8fe0642

Please sign in to comment.