Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 1 addition & 8 deletions src/engine/ExecuteUpdate.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,12 @@

#include <gtest/gtest_prod.h>

#include "engine/UpdateMetadata.h"
#include "index/Index.h"
#include "parser/ParsedQuery.h"
#include "util/CancellationHandle.h"
#include "util/TimeTracer.h"

// Metadata of a single update operation: number of inserted and deleted triples
// before the operation, of the operation, and after the operation.
struct UpdateMetadata {
std::optional<DeltaTriplesCount> countBefore_;
std::optional<DeltaTriplesCount> inUpdate_;
std::optional<DeltaTriplesCount> countAfter_;
};

class ExecuteUpdate {
public:
using CancellationHandle = ad_utility::SharedCancellationHandle;
Expand Down
53 changes: 52 additions & 1 deletion src/engine/GraphStoreProtocol.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include "engine/GraphStoreProtocol.h"

#include "parser/Tokenizer.h"
#include "util/Random.h"
#include "util/http/beast.h"

// ____________________________________________________________________________
Expand Down Expand Up @@ -84,6 +85,26 @@
blankNodeAdder.localVocab_.clone()};
}

// ____________________________________________________________________________
ResponseMiddleware GraphStoreProtocol::makePostNewGraphMiddleware(
const ad_utility::triple_component::Iri& newGraph) {
namespace http = boost::beast::http;
std::string location(asStringViewUnsafe(newGraph.getContent()));
return ResponseMiddleware(
[location](ResponseMiddleware::ResponseT&& response, auto) {

Check warning on line 94 in src/engine/GraphStoreProtocol.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

"std::move" is never called on this rvalue reference argument.

See more on https://sonarcloud.io/project/issues?id=ad-freiburg_qlever&issues=AZq2Cb064iflHjCUPTvz&open=AZq2Cb064iflHjCUPTvz&pullRequest=2532
response.result(http::status::created);
response.set(http::field::location, location);
return response;
});
}

// ____________________________________________________________________________
ad_utility::triple_component::Iri GraphStoreProtocol::generateGraphIri() {
ad_utility::SlowRandomIntGenerator<uint64_t> randGraphIriSuffix_;

Check warning on line 103 in src/engine/GraphStoreProtocol.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename this local variable "randGraphIriSuffix_" to match the regular expression: ^[a-z][a-zA-Z0-9]*$

See more on https://sonarcloud.io/project/issues?id=ad-freiburg_qlever&issues=AZq2Cb064iflHjCUPTv0&open=AZq2Cb064iflHjCUPTv0&pullRequest=2532
return ad_utility::triple_component::Iri::fromIriref(
QLEVER_NEW_GRAPH_PREFIX + std::to_string(randGraphIriSuffix_()) + ">");
}
Comment on lines 102 to 106
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: this might (though probably more in theory than in practice) generate the same graph twice making it incorrect. Keeping tracks of all graphs and/or explicit graph existence would go beyond the scope of this PR. As an intermediate solution the graphs will be generated with the internal prefix and a counter.


// ____________________________________________________________________________
ParsedQuery GraphStoreProtocol::transformGet(
const GraphOrDefault& graph, const EncodedIriManager* encodedIriManager) {
Expand All @@ -101,6 +122,22 @@
return SparqlParser::parseQuery(encodedIriManager, getQuery());
}

// ____________________________________________________________________________
ParsedQuery GraphStoreProtocol::transformHead(
const GraphOrDefault& graph, const EncodedIriManager* encodedIriManager) {
auto pq = transformGet(graph, encodedIriManager);
// HEAD does the same as GET except that the response has no body.
// Overwrite the body to be empty.
pq.responseMiddleware_ =
ResponseMiddleware([](ResponseMiddleware::ResponseT&& response) {

Check warning on line 132 in src/engine/GraphStoreProtocol.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

"std::move" is never called on this rvalue reference argument.

See more on https://sonarcloud.io/project/issues?id=ad-freiburg_qlever&issues=AZq2Cb064iflHjCUPTv1&open=AZq2Cb064iflHjCUPTv1&pullRequest=2532
response.body() = []() -> cppcoro::generator<std::string> {
co_return;
}();
return response;
});
return pq;
}

// ____________________________________________________________________________
ParsedQuery GraphStoreProtocol::transformDelete(const GraphOrDefault& graph,
const Index& index) {
Expand All @@ -114,8 +151,22 @@
return "DROP DEFAULT";
}
};
return ad_utility::getSingleElement(SparqlParser::parseUpdate(
auto update = ad_utility::getSingleElement(SparqlParser::parseUpdate(
index.getBlankNodeManager(), &index.encodedIriManager(), getUpdate()));
// DELETE must return 404 if the graph being deleted does not exist (GSP 5.4).
// With implicit graph existence a graph existed iff triples were actually
// deleted.
update.responseMiddleware_ = ResponseMiddleware(
[](ResponseMiddleware::ResponseT&& response, auto result) {

Check warning on line 160 in src/engine/GraphStoreProtocol.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

"std::move" is never called on this rvalue reference argument.

See more on https://sonarcloud.io/project/issues?id=ad-freiburg_qlever&issues=AZq2Cb064iflHjCUPTv2&open=AZq2Cb064iflHjCUPTv2&pullRequest=2532
AD_CORRECTNESS_CHECK(result.size() == 1);
if (result.at(0).inUpdate_->triplesDeleted_ == 0) {
response.result(boost::beast::http::status::not_found);
} else {
response.result(boost::beast::http::status::ok);
}
return response;
});
return update;
}

#endif
59 changes: 56 additions & 3 deletions src/engine/GraphStoreProtocol.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#include "parser/RdfParser.h"
#include "parser/SparqlParser.h"
#include "util/http/HttpUtils.h"
#include "util/http/ResponseMiddleware.h"
#include "util/http/UrlParser.h"

// Transform SPARQL Graph Store Protocol requests to their equivalent
Expand Down Expand Up @@ -95,20 +96,50 @@
Quads::BlankNodeAdder& blankNodeAdder);
FRIEND_TEST(GraphStoreProtocolTest, convertTriples);

static ResponseMiddleware makePostNewGraphMiddleware(
const ad_utility::triple_component::Iri& newGraph);

// Generates a random graph IRI. The IRI is generated randomly with an
// internal prefix. NOTE: It is not guaranteed that the IRI does not exist.
static ad_utility::triple_component::Iri generateGraphIri();

// Transform a SPARQL Graph Store Protocol POST to an equivalent ParsedQuery
// which is an SPARQL Update.
CPP_template_2(typename RequestT)(
requires ad_utility::httpUtils::HttpRequest<RequestT>) static ParsedQuery
transformPost(const RequestT& rawRequest, const GraphOrDefault& graph,
const Index& index) {
throwIfRequestBodyEmpty(rawRequest);
// For a `POST` when the graph identifies the QLever instance itself then
// the data must be stored in a newly generated graph which is returned in
// the response.
// TODO: test this with jena

Check warning on line 116 in src/engine/GraphStoreProtocol.h

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Complete the task associated to this "TODO" comment.

See more on https://sonarcloud.io/project/issues?id=ad-freiburg_qlever&issues=AZq2CbwG4iflHjCUPTvx&open=AZq2CbwG4iflHjCUPTvx&pullRequest=2532
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO

bool generateNewGraph = [&rawRequest, &graph]() {
if (std::holds_alternative<GraphRef>(graph) &&
rawRequest.find(boost::beast::http::field::host) !=
rawRequest.end()) {
return std::get<GraphRef>(graph) ==
ad_utility::triple_component::Iri::fromIriref(
"<http://" +
std::string(rawRequest[boost::beast::http::field::host]) +
"/" + GSP_DIRECT_GRAPH_IDENTIFICATION_PREFIX + ">");
}
return false;
}();
const GraphOrDefault effectiveGraph =
generateNewGraph ? generateGraphIri() : graph;
auto triples =
parseTriples(rawRequest.body(), extractMediatype(rawRequest));
Quads::BlankNodeAdder bn{{}, {}, index.getBlankNodeManager()};
auto convertedTriples = convertTriples(graph, std::move(triples), bn);
updateClause::GraphUpdate up{std::move(convertedTriples), {}};
ParsedQuery res;
res._clause = parsedQuery::UpdateClause{std::move(up)};
parsedQuery::UpdateClause clause{std::move(up)};
if (generateNewGraph) {
res.responseMiddleware_ = makePostNewGraphMiddleware(
std::get<ad_utility::triple_component::Iri>(effectiveGraph));
}
res._clause = std::move(clause);
// Graph store protocol POST requests might have a very large body. Limit
// the length used for the string representation.
res._originalString = truncatedStringRepresentation("POST", rawRequest);
Expand Down Expand Up @@ -136,11 +167,16 @@
}

// Transform a SPARQL Graph Store Protocol GET to an equivalent ParsedQuery
// which is an SPARQL Query.
// which is a SPARQL Query.
static ParsedQuery transformGet(const GraphOrDefault& graph,
const EncodedIriManager* encodedIriManager);
FRIEND_TEST(GraphStoreProtocolTest, transformGet);

// Transform a SPARQL Graph Store Protocol HEAD to an equivalent ParsedQuery.
// The response is the same as for GET but without the body.
static ParsedQuery transformHead(const GraphOrDefault& graph,
const EncodedIriManager* encodedIriManager);

// Transform a SPARQL Graph Store Protocol PUT to equivalent ParsedQueries
// which are SPARQL Updates.
CPP_template_2(typename RequestT)(
Expand Down Expand Up @@ -178,6 +214,23 @@
auto convertedTriples = convertTriples(graph, std::move(triples), bn);
updateClause::GraphUpdate up{std::move(convertedTriples), {}};
ParsedQuery insertData;
// Interpretation of the very vague GSP 5.3:
// - 201 Created if a new graph is created
// - 200 Ok or 204 No Content if an existing graph is modified
// When the drop (first operation) deletes triples then the graph has
// existed before in our model of implicit graph existence.
drop.responseMiddleware_ =
ResponseMiddleware([](ResponseMiddleware::ResponseT&& response,

Check warning on line 223 in src/engine/GraphStoreProtocol.h

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

"std::move" is never called on this rvalue reference argument.

See more on https://sonarcloud.io/project/issues?id=ad-freiburg_qlever&issues=AZq2CbwG4iflHjCUPTvy&open=AZq2CbwG4iflHjCUPTvy&pullRequest=2532
std::vector<UpdateMetadata> updateMetadata) {
AD_CORRECTNESS_CHECK(updateMetadata.size() == 2 &&
updateMetadata.at(0).inUpdate_.has_value());
if (updateMetadata.at(0).inUpdate_.value().triplesDeleted_ > 0) {
response.result(boost::beast::http::status::ok);
} else {
response.result(boost::beast::http::status::created);
}
return response;
});
insertData._clause = parsedQuery::UpdateClause{std::move(up)};
insertData._originalString = stringRepresentation;
return {std::move(drop), std::move(insertData)};
Expand Down Expand Up @@ -217,7 +270,7 @@
// DATA` of the payload.
return {transformTsop(rawRequest, operation.graph_, index)};
} else if (method == "HEAD") {
throwNotYetImplementedHTTPMethod("HEAD");
return {transformHead(operation.graph_, &index.encodedIriManager())};
} else if (method == "PATCH") {
throwNotYetImplementedHTTPMethod("PATCH");
} else {
Expand Down
28 changes: 24 additions & 4 deletions src/engine/Server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,7 @@ CPP_template_def(typename RequestT, typename ResponseT)(
//
// Some parameters require that "access-token" is set correctly. If not, that
// parameter is ignored.
std::optional<http::response<http::string_body>> response;
std::optional<http::response<streamable_body>> response;

// Execute commands (URL parameter with key "cmd").
auto logCommand = [](const std::optional<std::string_view>& cmd,
Expand Down Expand Up @@ -762,6 +762,10 @@ CPP_template_def(typename RequestT, typename ResponseT)(

auto response = ad_utility::httpUtils::createOkResponse(
std::move(responseGenerator), request, mediaType);
if (plannedQuery.parsedQuery_.responseMiddleware_.has_value()) {
response = plannedQuery.parsedQuery_.responseMiddleware_.value().apply(
std::move(response), std::nullopt);
}
try {
co_await send(std::move(response));
} catch (const boost::system::system_error& e) {
Expand Down Expand Up @@ -1036,14 +1040,23 @@ CPP_template_def(typename RequestT, typename ResponseT)(
AD_CORRECTNESS_CHECK(ql::ranges::all_of(
updates, [](const ParsedQuery& p) { return p.hasUpdateClause(); }));

std::vector<ResponseMiddleware> responseMiddlewares;
for (auto& update : updates) {
if (update.responseMiddleware_.has_value()) {
responseMiddlewares.push_back(
std::move(update.responseMiddleware_.value()));
}
}
std::vector<UpdateMetadata> metadatas;

// If multiple updates are part of a single request, those have to run
// atomically. This is ensured, because the updates below are run on the
// `updateThreadPool_`, which only has a single thread.
static_assert(UPDATE_THREAD_POOL_SIZE == 1);
auto coroutine = computeInNewThread(
updateThreadPool_,
[this, &requestTimer, &cancellationHandle, &updates, &qec, &timeLimit,
&plannedUpdate, tracer]() {
&plannedUpdate, tracer, &metadatas]() {
tracer->endTrace("waitingForUpdateThread");
json results = json::array();
// TODO<qup42> We currently create a new snapshot after each update in
Expand Down Expand Up @@ -1086,6 +1099,7 @@ CPP_template_def(typename RequestT, typename ResponseT)(
index_, index_.deltaTriplesManager().getCurrentSnapshot(),
*plannedUpdate, plannedUpdate->queryExecutionTree_,
updateMetadata, *tracer));
metadatas.push_back(std::move(updateMetadata));
tracer->reset();

AD_LOG_INFO << "Done processing update"
Expand All @@ -1105,8 +1119,14 @@ CPP_template_def(typename RequestT, typename ResponseT)(

// SPARQL 1.1 Protocol 2.2.4 Successful Responses: "The responses body of a
// successful update request is implementation defined."
co_await send(
ad_utility::httpUtils::createJsonResponse(std::move(responses), request));
auto response =
ad_utility::httpUtils::createJsonResponse(std::move(responses), request);
if (!responseMiddlewares.empty()) {
for (auto& middleware : responseMiddlewares) {
response = middleware.apply(std::move(response), std::move(metadatas));
}
}
co_await send(std::move(response));
co_return;
}

Expand Down
3 changes: 2 additions & 1 deletion src/engine/SparqlProtocol.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,8 @@ ad_utility::url_parser::ParsedRequest SparqlProtocol::parseHttpRequest(
// Graph Store Protocol with indirect graph identification
std::string_view methodStr = request.method_string();
if (request.method() == http::verb::put ||
request.method() == http::verb::delete_ || methodStr == "TSOP") {
request.method() == http::verb::delete_ ||
request.method() == http::verb::head || methodStr == "TSOP") {
return parseGraphStoreProtocolIndirect(request);
}
throw HttpError(
Expand Down
21 changes: 21 additions & 0 deletions src/engine/UpdateMetadata.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright 2025 The QLever Authors, in particular:
//
// 2025 Julian Mundhahs <[email protected]>, UFR
//
// UFR = University of Freiburg, Chair of Algorithms and Data Structures


#ifndef QLEVER_SRC_ENGINE_UPDATETYPES_H
#define QLEVER_SRC_ENGINE_UPDATETYPES_H

#include "index/DeltaTriples.h"

// Metadata of a single update operation: number of inserted and deleted triples
// before the operation, of the operation, and after the operation.
struct UpdateMetadata {
std::optional<DeltaTriplesCount> countBefore_;
std::optional<DeltaTriplesCount> inUpdate_;
std::optional<DeltaTriplesCount> countAfter_;
};

#endif //QLEVER_SRC_ENGINE_UPDATETYPES_H
10 changes: 10 additions & 0 deletions src/global/Constants.h
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,16 @@ constexpr inline std::string_view QLEVER_INTERNAL_BLANK_NODE_IRI_PREFIX =
QLEVER_INTERNAL_PREFIX_URL,
string_constants::detail::blank_node_prefix>();

// The prefix of the new graph IRIs that are generated when a Graph Store
// Protocol PUT is made without specifying a graph.
namespace string_constants::detail {
constexpr inline std::string_view new_graph_prefix = "graphs/";
} // namespace string_constants::detail
constexpr inline std::string_view QLEVER_NEW_GRAPH_PREFIX =
ad_utility::constexprStrCat<string_constants::detail::openAngle,
QLEVER_INTERNAL_PREFIX_URL,
string_constants::detail::new_graph_prefix>();

// The prefix of the SERVICE IRI used for a cached result with a name. Use as
// in `SERVICE <ql:cached-result-with-name-$query-name$> {}`.
namespace string_constants::detail {
Expand Down
5 changes: 5 additions & 0 deletions src/parser/ParsedQuery.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
#include "parser/data/OrderKey.h"
#include "parser/data/SolutionModifiers.h"
#include "parser/data/SparqlFilter.h"
#include "util/http/ResponseMiddleware.h"

// Data container for prefixes
class SparqlPrefix {
Expand Down Expand Up @@ -90,6 +91,10 @@ class ParsedQuery {
// The IRIs from the FROM and FROM NAMED clauses.
DatasetClauses datasetClauses_;

// A function to modify the HTTP response for this operation before it is
// sent.
std::optional<ResponseMiddleware> responseMiddleware_;

[[nodiscard]] bool hasSelectClause() const {
return std::holds_alternative<SelectClause>(_clause);
}
Expand Down
5 changes: 4 additions & 1 deletion src/util/http/HttpServer.h
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,10 @@ CPP_template(typename HttpHandler, typename WebSocketHandler)(
auto errorResponse = ad_utility::websocket::WebSocketSession::
getErrorResponseIfPathIsInvalid(req);
if (errorResponse.has_value()) {
co_await sendMessage(errorResponse.value());
// `errorResponse` can safely be moved because this declaration is
// not used anymore in this scope. After the scope the outer
// declaration becomes visible again.
co_await sendMessage(std::move(errorResponse.value()));
} else {
// prevent cleanup after socket has been moved from
releaseConnection.cancel();
Expand Down
Loading
Loading