mirror of https://gitee.com/bigwinds/arangodb
970 lines
33 KiB
C++
970 lines
33 KiB
C++
////////////////////////////////////////////////////////////////////////////////
|
|
/// @brief cursor request handler
|
|
///
|
|
/// @file
|
|
///
|
|
/// DISCLAIMER
|
|
///
|
|
/// Copyright 2014 ArangoDB GmbH, Cologne, Germany
|
|
/// Copyright 2004-2014 triAGENS GmbH, Cologne, Germany
|
|
///
|
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|
/// you may not use this file except in compliance with the License.
|
|
/// You may obtain a copy of the License at
|
|
///
|
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
|
///
|
|
/// Unless required by applicable law or agreed to in writing, software
|
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
/// See the License for the specific language governing permissions and
|
|
/// limitations under the License.
|
|
///
|
|
/// Copyright holder is ArangoDB GmbH, Cologne, Germany
|
|
///
|
|
/// @author Jan Steemann
|
|
/// @author Copyright 2014, ArangoDB GmbH, Cologne, Germany
|
|
/// @author Copyright 2010-2014, triAGENS GmbH, Cologne, Germany
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
#include "RestCursorHandler.h"
|
|
#include "Aql/Query.h"
|
|
#include "Aql/QueryRegistry.h"
|
|
#include "Basics/Exceptions.h"
|
|
#include "Basics/json.h"
|
|
#include "Basics/MutexLocker.h"
|
|
#include "Basics/ScopeGuard.h"
|
|
#include "Utils/Cursor.h"
|
|
#include "Utils/CursorRepository.h"
|
|
#include "V8Server/ApplicationV8.h"
|
|
|
|
using namespace triagens::arango;
|
|
using namespace triagens::rest;
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// --SECTION-- constructors and destructors
|
|
// -----------------------------------------------------------------------------
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
/// @brief constructor
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
RestCursorHandler::RestCursorHandler (HttpRequest* request,
|
|
std::pair<triagens::arango::ApplicationV8*, triagens::aql::QueryRegistry*>* pair)
|
|
: RestVocbaseBaseHandler(request),
|
|
_applicationV8(pair->first),
|
|
_queryRegistry(pair->second),
|
|
_queryLock(),
|
|
_query(nullptr),
|
|
_queryKilled(false) {
|
|
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// --SECTION-- Handler methods
|
|
// -----------------------------------------------------------------------------
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
/// {@inheritDoc}
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
HttpHandler::status_t RestCursorHandler::execute () {
|
|
// extract the sub-request type
|
|
HttpRequest::HttpRequestType type = _request->requestType();
|
|
|
|
if (type == HttpRequest::HTTP_REQUEST_POST) {
|
|
createCursor();
|
|
return status_t(HANDLER_DONE);
|
|
}
|
|
|
|
if (type == HttpRequest::HTTP_REQUEST_PUT) {
|
|
modifyCursor();
|
|
return status_t(HANDLER_DONE);
|
|
}
|
|
|
|
if (type == HttpRequest::HTTP_REQUEST_DELETE) {
|
|
deleteCursor();
|
|
return status_t(HANDLER_DONE);
|
|
}
|
|
|
|
generateError(HttpResponse::METHOD_NOT_ALLOWED, TRI_ERROR_HTTP_METHOD_NOT_ALLOWED);
|
|
return status_t(HANDLER_DONE);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
/// {@inheritDoc}
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
bool RestCursorHandler::cancel (bool running) {
|
|
if (running) {
|
|
cancelQuery();
|
|
return true;
|
|
}
|
|
|
|
generateCanceled();
|
|
return true;
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// --SECTION-- private methods
|
|
// -----------------------------------------------------------------------------
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
/// @brief register the currently running query
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
void RestCursorHandler::registerQuery (triagens::aql::Query* query) {
|
|
MUTEX_LOCKER(_queryLock);
|
|
|
|
TRI_ASSERT(_query == nullptr);
|
|
_query = query;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
/// @brief unregister the currently running query
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
void RestCursorHandler::unregisterQuery () {
|
|
MUTEX_LOCKER(_queryLock);
|
|
|
|
_query = nullptr;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
/// @brief cancel the currently running query
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
bool RestCursorHandler::cancelQuery () {
|
|
MUTEX_LOCKER(_queryLock);
|
|
|
|
if (_query != nullptr) {
|
|
_query->killed(true);
|
|
_queryKilled = true;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
/// @brief whether or not the query was cancelled
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
bool RestCursorHandler::wasCancelled () {
|
|
MUTEX_LOCKER(_queryLock);
|
|
return _queryKilled;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
/// @brief build options for the query as JSON
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
triagens::basics::Json RestCursorHandler::buildOptions (TRI_json_t const* json) {
|
|
auto getAttribute = [&json] (char const* name) {
|
|
return TRI_LookupObjectJson(json, name);
|
|
};
|
|
|
|
triagens::basics::Json options(triagens::basics::Json::Object, 3);
|
|
|
|
auto attribute = getAttribute("count");
|
|
options.set("count", triagens::basics::Json(TRI_IsBooleanJson(attribute) ? attribute->_value._boolean : false));
|
|
|
|
attribute = getAttribute("batchSize");
|
|
options.set("batchSize", triagens::basics::Json(TRI_IsNumberJson(attribute) ? attribute->_value._number : 1000.0));
|
|
|
|
if (TRI_IsNumberJson(attribute) && static_cast<size_t>(attribute->_value._number) == 0) {
|
|
THROW_ARANGO_EXCEPTION_MESSAGE(TRI_ERROR_TYPE_ERROR, "expecting non-zero value for <batchSize>");
|
|
}
|
|
|
|
attribute = getAttribute("options");
|
|
|
|
if (TRI_IsObjectJson(attribute)) {
|
|
size_t const n = TRI_LengthVector(&attribute->_value._objects);
|
|
|
|
for (size_t i = 0; i < n; i += 2) {
|
|
auto key = static_cast<TRI_json_t const*>(TRI_AtVector(&attribute->_value._objects, i));
|
|
auto value = static_cast<TRI_json_t const*>(TRI_AtVector(&attribute->_value._objects, i + 1));
|
|
|
|
if (! TRI_IsStringJson(key) || value == nullptr) {
|
|
continue;
|
|
}
|
|
|
|
auto keyName = key->_value._string.data;
|
|
|
|
if (strcmp(keyName, "count") != 0 &&
|
|
strcmp(keyName, "batchSize") != 0) {
|
|
options.set(keyName, triagens::basics::Json(
|
|
TRI_UNKNOWN_MEM_ZONE,
|
|
TRI_CopyJson(TRI_UNKNOWN_MEM_ZONE, value),
|
|
triagens::basics::Json::NOFREE
|
|
));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (! options.has("ttl")) {
|
|
attribute = getAttribute("ttl");
|
|
options.set("ttl", triagens::basics::Json(TRI_IsNumberJson(attribute) ? attribute->_value._number : 30.0));
|
|
}
|
|
|
|
return options;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
/// @brief builds the "extra" attribute values from the result.
|
|
/// note that the "extra" object will take ownership from the result for
|
|
/// several values
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
triagens::basics::Json RestCursorHandler::buildExtra (triagens::aql::QueryResult& queryResult) {
|
|
// build "extra" attribute
|
|
triagens::basics::Json extra(triagens::basics::Json::Object);
|
|
|
|
if (queryResult.stats != nullptr) {
|
|
extra.set("stats", triagens::basics::Json(TRI_UNKNOWN_MEM_ZONE, queryResult.stats, triagens::basics::Json::AUTOFREE));
|
|
queryResult.stats = nullptr;
|
|
}
|
|
if (queryResult.profile != nullptr) {
|
|
extra.set("profile", triagens::basics::Json(TRI_UNKNOWN_MEM_ZONE, queryResult.profile, triagens::basics::Json::AUTOFREE));
|
|
queryResult.profile = nullptr;
|
|
}
|
|
if (queryResult.warnings == nullptr) {
|
|
extra.set("warnings", triagens::basics::Json(triagens::basics::Json::Array));
|
|
}
|
|
else {
|
|
extra.set("warnings", triagens::basics::Json(TRI_UNKNOWN_MEM_ZONE, queryResult.warnings, triagens::basics::Json::AUTOFREE));
|
|
queryResult.warnings = nullptr;
|
|
}
|
|
|
|
return extra;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
/// @startDocuBlock JSF_post_api_cursor
|
|
/// @brief create a cursor and return the first results
|
|
///
|
|
/// @RESTHEADER{POST /_api/cursor, Create cursor}
|
|
///
|
|
/// @RESTBODYPARAM{query,json,required}
|
|
/// A JSON object describing the query and query parameters.
|
|
///
|
|
/// @RESTDESCRIPTION
|
|
/// The query details include the query string plus optional query options and
|
|
/// bind parameters. These values need to be passed in a JSON representation in
|
|
/// the body of the POST request.
|
|
///
|
|
/// The following attributes can be used inside the JSON object:
|
|
///
|
|
/// - *query*: contains the query string to be executed (mandatory)
|
|
///
|
|
/// - *count*: boolean flag that indicates whether the number of documents
|
|
/// in the result set should be returned in the "count" attribute of the result (optional).
|
|
/// Calculating the "count" attribute might in the future have a performance
|
|
/// impact for some queries so this option is turned off by default, and "count"
|
|
/// is only returned when requested.
|
|
///
|
|
/// - *batchSize*: maximum number of result documents to be transferred from
|
|
/// the server to the client in one roundtrip (optional). If this attribute is
|
|
/// not set, a server-controlled default value will be used.
|
|
///
|
|
/// - *ttl*: an optional time-to-live for the cursor (in seconds). The cursor will be
|
|
/// removed on the server automatically after the specified amount of time. This
|
|
/// is useful to ensure garbage collection of cursors that are not fully fetched
|
|
/// by clients. If not set, a server-defined value will be used.
|
|
///
|
|
/// - *bindVars*: key/value list of bind parameters (optional).
|
|
///
|
|
/// - *options*: key/value list of extra options for the query (optional).
|
|
///
|
|
/// The following options are supported at the moment:
|
|
///
|
|
/// - *fullCount*: if set to *true* and the query contains a *LIMIT* clause, then the
|
|
/// result will contain an extra attribute *extra* with a sub-attribute *fullCount*.
|
|
/// This sub-attribute will contain the number of documents in the result before the
|
|
/// last LIMIT in the query was applied. It can be used to count the number of documents that
|
|
/// match certain filter criteria, but only return a subset of them, in one go.
|
|
/// It is thus similar to MySQL's *SQL_CALC_FOUND_ROWS* hint. Note that setting the option
|
|
/// will disable a few LIMIT optimizations and may lead to more documents being processed,
|
|
/// and thus make queries run longer. Note that the *fullCount* sub-attribute will only
|
|
/// be present in the result if the query has a LIMIT clause and the LIMIT clause is
|
|
/// actually used in the query.
|
|
///
|
|
/// - *maxPlans*: limits the maximum number of plans that are created by the AQL
|
|
/// query optimizer.
|
|
///
|
|
/// - *optimizer.rules*: a list of to-be-included or to-be-excluded optimizer rules
|
|
/// can be put into this attribute, telling the optimizer to include or exclude
|
|
/// specific rules. To disable a rule, prefix its name with a `-`, to enable a rule, prefix it
|
|
/// with a `+`. There is also a pseudo-rule `all`, which will match all optimizer rules.
|
|
///
|
|
/// If the result set can be created by the server, the server will respond with
|
|
/// *HTTP 201*. The body of the response will contain a JSON object with the
|
|
/// result set.
|
|
///
|
|
/// The returned JSON object has the following properties:
|
|
///
|
|
/// - *error*: boolean flag to indicate that an error occurred (*false*
|
|
/// in this case)
|
|
///
|
|
/// - *code*: the HTTP status code
|
|
///
|
|
/// - *result*: an array of result documents (might be empty if query has no results)
|
|
///
|
|
/// - *hasMore*: a boolean indicator whether there are more results
|
|
/// available for the cursor on the server
|
|
///
|
|
/// - *count*: the total number of result documents available (only
|
|
/// available if the query was executed with the *count* attribute set)
|
|
///
|
|
/// - *id*: id of temporary cursor created on the server (optional, see above)
|
|
///
|
|
/// - *extra*: an optional JSON object with extra information about the query result.
|
|
/// For data-modification queries, the *extra* attribute will contain the number
|
|
/// of modified documents and the number of documents that could not be modified
|
|
/// due to an error (if *ignoreErrors* query option is specified)
|
|
///
|
|
/// If the JSON representation is malformed or the query specification is
|
|
/// missing from the request, the server will respond with *HTTP 400*.
|
|
///
|
|
/// The body of the response will contain a JSON object with additional error
|
|
/// details. The object has the following attributes:
|
|
///
|
|
/// - *error*: boolean flag to indicate that an error occurred (*true* in this case)
|
|
///
|
|
/// - *code*: the HTTP status code
|
|
///
|
|
/// - *errorNum*: the server error number
|
|
///
|
|
/// - *errorMessage*: a descriptive error message
|
|
///
|
|
/// If the query specification is complete, the server will process the query. If an
|
|
/// error occurs during query processing, the server will respond with *HTTP 400*.
|
|
/// Again, the body of the response will contain details about the error.
|
|
///
|
|
/// A list of query errors can be found (../ArangoErrors/README.md) here.
|
|
///
|
|
/// @RESTRETURNCODES
|
|
///
|
|
/// @RESTRETURNCODE{201}
|
|
/// is returned if the result set can be created by the server.
|
|
///
|
|
/// @RESTRETURNCODE{400}
|
|
/// is returned if the JSON representation is malformed or the query specification is
|
|
/// missing from the request.
|
|
///
|
|
/// @RESTRETURNCODE{404}
|
|
/// The server will respond with *HTTP 404* in case a non-existing collection is
|
|
/// accessed in the query.
|
|
///
|
|
/// @RESTRETURNCODE{405}
|
|
/// The server will respond with *HTTP 405* if an unsupported HTTP method is used.
|
|
///
|
|
/// @EXAMPLES
|
|
///
|
|
/// Executes a query and extract the result in a single go:
|
|
///
|
|
/// @EXAMPLE_ARANGOSH_RUN{RestCursorCreateCursorForLimitReturnSingle}
|
|
/// var cn = "products";
|
|
/// db._drop(cn);
|
|
/// db._create(cn);
|
|
///
|
|
/// db.products.save({"hello1":"world1"});
|
|
/// db.products.save({"hello2":"world1"});
|
|
///
|
|
/// var url = "/_api/cursor";
|
|
/// var body = {
|
|
/// query: "FOR p IN products LIMIT 2 RETURN p",
|
|
/// count: true,
|
|
/// batchSize: 2
|
|
/// };
|
|
///
|
|
/// var response = logCurlRequest('POST', url, JSON.stringify(body));
|
|
///
|
|
/// assert(response.code === 201);
|
|
///
|
|
/// logJsonResponse(response);
|
|
/// ~ db._drop(cn);
|
|
/// @END_EXAMPLE_ARANGOSH_RUN
|
|
///
|
|
/// Executes a query and extracts part of the result:
|
|
///
|
|
/// @EXAMPLE_ARANGOSH_RUN{RestCursorCreateCursorForLimitReturn}
|
|
/// var cn = "products";
|
|
/// db._drop(cn);
|
|
/// db._create(cn);
|
|
///
|
|
/// db.products.save({"hello1":"world1"});
|
|
/// db.products.save({"hello2":"world1"});
|
|
/// db.products.save({"hello3":"world1"});
|
|
/// db.products.save({"hello4":"world1"});
|
|
/// db.products.save({"hello5":"world1"});
|
|
///
|
|
/// var url = "/_api/cursor";
|
|
/// var body = {
|
|
/// query: "FOR p IN products LIMIT 5 RETURN p",
|
|
/// count: true,
|
|
/// batchSize: 2
|
|
/// };
|
|
///
|
|
/// var response = logCurlRequest('POST', url, JSON.stringify(body));
|
|
///
|
|
/// assert(response.code === 201);
|
|
///
|
|
/// logJsonResponse(response);
|
|
/// ~ db._drop(cn);
|
|
/// @END_EXAMPLE_ARANGOSH_RUN
|
|
///
|
|
/// Using query option "fullCount":
|
|
///
|
|
/// @EXAMPLE_ARANGOSH_RUN{RestCursorCreateCursorOption}
|
|
/// var url = "/_api/cursor";
|
|
/// var body = {
|
|
/// query: "FOR i IN 1..1000 FILTER i > 500 LIMIT 10 RETURN i",
|
|
/// count: true,
|
|
/// options: {
|
|
/// fullCount: true
|
|
/// }
|
|
/// };
|
|
///
|
|
/// var response = logCurlRequest('POST', url, JSON.stringify(body));
|
|
///
|
|
/// assert(response.code === 201);
|
|
///
|
|
/// logJsonResponse(response);
|
|
/// @END_EXAMPLE_ARANGOSH_RUN
|
|
///
|
|
/// Enabling and disabling optimizer rules:
|
|
///
|
|
/// @EXAMPLE_ARANGOSH_RUN{RestCursorOptimizerRules}
|
|
/// var url = "/_api/cursor";
|
|
/// var body = {
|
|
/// query: "FOR i IN 1..10 LET a = 1 LET b = 2 FILTER a + b == 3 RETURN i",
|
|
/// count: true,
|
|
/// options: {
|
|
/// maxPlans: 1,
|
|
/// optimizer: {
|
|
/// rules: [ "-all", "+remove-unnecessary-filters" ]
|
|
/// }
|
|
/// }
|
|
/// };
|
|
///
|
|
/// var response = logCurlRequest('POST', url, JSON.stringify(body));
|
|
///
|
|
/// assert(response.code === 201);
|
|
///
|
|
/// logJsonResponse(response);
|
|
/// @END_EXAMPLE_ARANGOSH_RUN
|
|
///
|
|
/// Executes a data-modification query and retrieves the number of
|
|
/// modified documents:
|
|
///
|
|
/// @EXAMPLE_ARANGOSH_RUN{RestCursorDeleteQuery}
|
|
/// var cn = "products";
|
|
/// db._drop(cn);
|
|
/// db._create(cn);
|
|
///
|
|
/// db.products.save({"hello1":"world1"});
|
|
/// db.products.save({"hello2":"world1"});
|
|
///
|
|
/// var url = "/_api/cursor";
|
|
/// var body = {
|
|
/// query: "FOR p IN products REMOVE p IN products"
|
|
/// };
|
|
///
|
|
/// var response = logCurlRequest('POST', url, JSON.stringify(body));
|
|
///
|
|
/// assert(response.code === 201);
|
|
/// assert(JSON.parse(response.body).extra.stats.writesExecuted === 2);
|
|
/// assert(JSON.parse(response.body).extra.stats.writesIgnored === 0);
|
|
///
|
|
/// logJsonResponse(response);
|
|
/// ~ db._drop(cn);
|
|
/// @END_EXAMPLE_ARANGOSH_RUN
|
|
///
|
|
/// Executes a data-modification query with option *ignoreErrors*:
|
|
///
|
|
/// @EXAMPLE_ARANGOSH_RUN{RestCursorDeleteIgnore}
|
|
/// var cn = "products";
|
|
/// db._drop(cn);
|
|
/// db._create(cn);
|
|
///
|
|
/// db.products.save({ _key: "foo" });
|
|
///
|
|
/// var url = "/_api/cursor";
|
|
/// var body = {
|
|
/// query: "REMOVE 'bar' IN products OPTIONS { ignoreErrors: true }"
|
|
/// };
|
|
///
|
|
/// var response = logCurlRequest('POST', url, JSON.stringify(body));
|
|
///
|
|
/// assert(response.code === 201);
|
|
/// assert(JSON.parse(response.body).extra.stats.writesExecuted === 0);
|
|
/// assert(JSON.parse(response.body).extra.stats.writesIgnored === 1);
|
|
///
|
|
/// logJsonResponse(response);
|
|
/// ~ db._drop(cn);
|
|
/// @END_EXAMPLE_ARANGOSH_RUN
|
|
///
|
|
/// Bad queries:
|
|
///
|
|
/// Missing body:
|
|
///
|
|
/// @EXAMPLE_ARANGOSH_RUN{RestCursorCreateCursorMissingBody}
|
|
/// var url = "/_api/cursor";
|
|
///
|
|
/// var response = logCurlRequest('POST', url, '');
|
|
///
|
|
/// assert(response.code === 400);
|
|
///
|
|
/// logJsonResponse(response);
|
|
/// @END_EXAMPLE_ARANGOSH_RUN
|
|
///
|
|
/// Unknown collection:
|
|
///
|
|
/// @EXAMPLE_ARANGOSH_RUN{RestCursorCreateCursorUnknownCollection}
|
|
/// var url = "/_api/cursor";
|
|
/// var body = {
|
|
/// query: "FOR u IN unknowncoll LIMIT 2 RETURN u",
|
|
/// count: true,
|
|
/// batchSize: 2
|
|
/// };
|
|
///
|
|
/// var response = logCurlRequest('POST', url, JSON.stringify(body));
|
|
///
|
|
/// assert(response.code === 404);
|
|
///
|
|
/// logJsonResponse(response);
|
|
/// @END_EXAMPLE_ARANGOSH_RUN
|
|
///
|
|
/// Executes a data-modification query that attempts to remove a non-existing
|
|
/// document:
|
|
///
|
|
/// @EXAMPLE_ARANGOSH_RUN{RestCursorDeleteQueryFail}
|
|
/// var cn = "products";
|
|
/// db._drop(cn);
|
|
/// db._create(cn);
|
|
///
|
|
/// db.products.save({ _key: "bar" });
|
|
///
|
|
/// var url = "/_api/cursor";
|
|
/// var body = {
|
|
/// query: "REMOVE 'foo' IN products"
|
|
/// };
|
|
///
|
|
/// var response = logCurlRequest('POST', url, JSON.stringify(body));
|
|
///
|
|
/// assert(response.code === 404);
|
|
///
|
|
/// logJsonResponse(response);
|
|
/// ~ db._drop(cn);
|
|
/// @END_EXAMPLE_ARANGOSH_RUN
|
|
///
|
|
/// @endDocuBlock
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
void RestCursorHandler::createCursor () {
|
|
std::vector<std::string> const& suffix = _request->suffix();
|
|
|
|
if (suffix.size() != 0) {
|
|
generateError(HttpResponse::BAD,
|
|
TRI_ERROR_HTTP_BAD_PARAMETER,
|
|
"expecting POST /_api/cursor");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
std::unique_ptr<TRI_json_t> json(parseJsonBody());
|
|
|
|
if (json.get() == nullptr) {
|
|
return;
|
|
}
|
|
|
|
if (! TRI_IsObjectJson(json.get())) {
|
|
generateError(HttpResponse::BAD, TRI_ERROR_QUERY_EMPTY);
|
|
return;
|
|
}
|
|
|
|
auto const* queryString = TRI_LookupObjectJson(json.get(), "query");
|
|
|
|
if (! TRI_IsStringJson(queryString)) {
|
|
generateError(HttpResponse::BAD, TRI_ERROR_QUERY_EMPTY);
|
|
return;
|
|
}
|
|
|
|
auto const* bindVars = TRI_LookupObjectJson(json.get(), "bindVars");
|
|
|
|
if (bindVars != nullptr) {
|
|
if (! TRI_IsObjectJson(bindVars) &&
|
|
! TRI_IsNullJson(bindVars)) {
|
|
generateError(HttpResponse::BAD, TRI_ERROR_TYPE_ERROR, "expecting object for <bindVars>");
|
|
return;
|
|
}
|
|
}
|
|
|
|
auto options = buildOptions(json.get());
|
|
|
|
triagens::aql::Query query(_applicationV8,
|
|
false,
|
|
_vocbase,
|
|
queryString->_value._string.data,
|
|
static_cast<size_t>(queryString->_value._string.length - 1),
|
|
(bindVars != nullptr ? TRI_CopyJson(TRI_UNKNOWN_MEM_ZONE, bindVars) : nullptr),
|
|
TRI_CopyJson(TRI_UNKNOWN_MEM_ZONE, options.json()),
|
|
triagens::aql::PART_MAIN);
|
|
|
|
registerQuery(&query);
|
|
auto queryResult = query.execute(_queryRegistry);
|
|
unregisterQuery();
|
|
|
|
if (queryResult.code != TRI_ERROR_NO_ERROR) {
|
|
if (queryResult.code == TRI_ERROR_REQUEST_CANCELED ||
|
|
(queryResult.code == TRI_ERROR_QUERY_KILLED && wasCancelled())) {
|
|
THROW_ARANGO_EXCEPTION(TRI_ERROR_REQUEST_CANCELED);
|
|
}
|
|
|
|
THROW_ARANGO_EXCEPTION_MESSAGE(queryResult.code, queryResult.details);
|
|
}
|
|
|
|
TRI_ASSERT(TRI_IsArrayJson(queryResult.json));
|
|
|
|
{
|
|
_response = createResponse(HttpResponse::CREATED);
|
|
_response->setContentType("application/json; charset=utf-8");
|
|
|
|
// build "extra" attribute
|
|
triagens::basics::Json extra(triagens::basics::Json::Object, 3);
|
|
|
|
if (queryResult.stats != nullptr) {
|
|
extra.set("stats", triagens::basics::Json(TRI_UNKNOWN_MEM_ZONE, queryResult.stats, triagens::basics::Json::AUTOFREE));
|
|
queryResult.stats = nullptr;
|
|
}
|
|
if (queryResult.profile != nullptr) {
|
|
extra.set("profile", triagens::basics::Json(TRI_UNKNOWN_MEM_ZONE, queryResult.profile, triagens::basics::Json::AUTOFREE));
|
|
queryResult.profile = nullptr;
|
|
}
|
|
if (queryResult.warnings == nullptr) {
|
|
extra.set("warnings", triagens::basics::Json(triagens::basics::Json::Array));
|
|
}
|
|
else {
|
|
extra.set("warnings", triagens::basics::Json(TRI_UNKNOWN_MEM_ZONE, queryResult.warnings, triagens::basics::Json::AUTOFREE));
|
|
queryResult.warnings = nullptr;
|
|
}
|
|
|
|
|
|
size_t batchSize = triagens::basics::JsonHelper::getNumericValue<size_t>(options.json(), "batchSize", 1000);
|
|
size_t const n = TRI_LengthArrayJson(queryResult.json);
|
|
|
|
if (n <= batchSize) {
|
|
// result is smaller than batchSize and will be returned directly. no need to create a cursor
|
|
|
|
triagens::basics::Json result(triagens::basics::Json::Object, 6);
|
|
result.set("result", triagens::basics::Json(TRI_UNKNOWN_MEM_ZONE, queryResult.json, triagens::basics::Json::AUTOFREE));
|
|
queryResult.json = nullptr;
|
|
|
|
result.set("hasMore", triagens::basics::Json(false));
|
|
|
|
if (triagens::basics::JsonHelper::getBooleanValue(options.json(), "count", false)) {
|
|
result.set("count", triagens::basics::Json(static_cast<double>(n)));
|
|
}
|
|
|
|
result.set("extra", extra);
|
|
result.set("error", triagens::basics::Json(false));
|
|
result.set("code", triagens::basics::Json(static_cast<double>(_response->responseCode())));
|
|
|
|
result.dump(_response->body());
|
|
return;
|
|
}
|
|
|
|
// result is bigger than batchSize, and a cursor will be created
|
|
auto cursors = static_cast<triagens::arango::CursorRepository*>(_vocbase->_cursorRepository);
|
|
TRI_ASSERT(cursors != nullptr);
|
|
|
|
double ttl = triagens::basics::JsonHelper::getNumericValue<double>(options.json(), "ttl", 30);
|
|
bool count = triagens::basics::JsonHelper::getBooleanValue(options.json(), "count", false);
|
|
|
|
// steal the query JSON, cursor will take over the ownership
|
|
auto j = queryResult.json;
|
|
triagens::arango::JsonCursor* cursor = cursors->createFromJson(j, batchSize, extra.steal(), ttl, count);
|
|
queryResult.json = nullptr;
|
|
|
|
try {
|
|
_response->body().appendChar('{');
|
|
cursor->dump(_response->body());
|
|
_response->body().appendText(",\"error\":false,\"code\":");
|
|
_response->body().appendInteger(static_cast<uint32_t>(_response->responseCode()));
|
|
_response->body().appendChar('}');
|
|
|
|
cursors->release(cursor);
|
|
}
|
|
catch (...) {
|
|
cursors->release(cursor);
|
|
throw;
|
|
}
|
|
}
|
|
}
|
|
catch (triagens::basics::Exception const& ex) {
|
|
unregisterQuery();
|
|
generateError(HttpResponse::responseCode(ex.code()), ex.code(), ex.what());
|
|
}
|
|
catch (...) {
|
|
unregisterQuery();
|
|
generateError(HttpResponse::SERVER_ERROR, TRI_ERROR_INTERNAL);
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
/// @startDocuBlock JSF_post_api_cursor_identifier
|
|
/// @brief return the next results from an existing cursor
|
|
///
|
|
/// @RESTHEADER{PUT /_api/cursor/{cursor-identifier}, Read next batch from cursor}
|
|
///
|
|
/// @RESTURLPARAMETERS
|
|
///
|
|
/// @RESTURLPARAM{cursor-identifier,string,required}
|
|
/// The name of the cursor
|
|
///
|
|
/// @RESTDESCRIPTION
|
|
///
|
|
/// If the cursor is still alive, returns an object with the following
|
|
/// attributes:
|
|
///
|
|
/// - *id*: the *cursor-identifier*
|
|
/// - *result*: a list of documents for the current batch
|
|
/// - *hasMore*: *false* if this was the last batch
|
|
/// - *count*: if present the total number of elements
|
|
///
|
|
/// Note that even if *hasMore* returns *true*, the next call might
|
|
/// still return no documents. If, however, *hasMore* is *false*, then
|
|
/// the cursor is exhausted. Once the *hasMore* attribute has a value of
|
|
/// *false*, the client can stop.
|
|
///
|
|
/// @RESTRETURNCODES
|
|
///
|
|
/// @RESTRETURNCODE{200}
|
|
/// The server will respond with *HTTP 200* in case of success.
|
|
///
|
|
/// @RESTRETURNCODE{400}
|
|
/// If the cursor identifier is omitted, the server will respond with *HTTP 404*.
|
|
///
|
|
/// @RESTRETURNCODE{404}
|
|
/// If no cursor with the specified identifier can be found, the server will respond
|
|
/// with *HTTP 404*.
|
|
///
|
|
/// @EXAMPLES
|
|
///
|
|
/// Valid request for next batch:
|
|
///
|
|
/// @EXAMPLE_ARANGOSH_RUN{RestCursorForLimitReturnCont}
|
|
/// var url = "/_api/cursor";
|
|
/// var cn = "products";
|
|
/// db._drop(cn);
|
|
/// db._create(cn);
|
|
///
|
|
/// db.products.save({"hello1":"world1"});
|
|
/// db.products.save({"hello2":"world1"});
|
|
/// db.products.save({"hello3":"world1"});
|
|
/// db.products.save({"hello4":"world1"});
|
|
/// db.products.save({"hello5":"world1"});
|
|
///
|
|
/// var url = "/_api/cursor";
|
|
/// var body = {
|
|
/// query: "FOR p IN products LIMIT 5 RETURN p",
|
|
/// count: true,
|
|
/// batchSize: 2
|
|
/// };
|
|
/// var response = logCurlRequest('POST', url, JSON.stringify(body));
|
|
///
|
|
/// var body = response.body.replace(/\\/g, '');
|
|
/// var _id = JSON.parse(body).id;
|
|
/// response = logCurlRequest('PUT', url + '/' + _id, '');
|
|
/// assert(response.code === 200);
|
|
///
|
|
/// logJsonResponse(response);
|
|
/// ~ db._drop(cn);
|
|
/// @END_EXAMPLE_ARANGOSH_RUN
|
|
///
|
|
/// Missing identifier
|
|
///
|
|
/// @EXAMPLE_ARANGOSH_RUN{RestCursorMissingCursorIdentifier}
|
|
/// var url = "/_api/cursor";
|
|
///
|
|
/// var response = logCurlRequest('PUT', url, '');
|
|
///
|
|
/// assert(response.code === 400);
|
|
///
|
|
/// logJsonResponse(response);
|
|
/// @END_EXAMPLE_ARANGOSH_RUN
|
|
///
|
|
/// Unknown identifier
|
|
///
|
|
/// @EXAMPLE_ARANGOSH_RUN{RestCursorInvalidCursorIdentifier}
|
|
/// var url = "/_api/cursor/123123";
|
|
///
|
|
/// var response = logCurlRequest('PUT', url, '');
|
|
///
|
|
/// assert(response.code === 404);
|
|
///
|
|
/// logJsonResponse(response);
|
|
/// @END_EXAMPLE_ARANGOSH_RUN
|
|
/// @endDocuBlock
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
void RestCursorHandler::modifyCursor () {
|
|
std::vector<std::string> const& suffix = _request->suffix();
|
|
|
|
if (suffix.size() != 1) {
|
|
generateError(HttpResponse::BAD,
|
|
TRI_ERROR_HTTP_BAD_PARAMETER,
|
|
"expecting PUT /_api/cursor/<cursor-id>");
|
|
return;
|
|
}
|
|
|
|
std::string const& id = suffix[0];
|
|
|
|
auto cursors = static_cast<triagens::arango::CursorRepository*>(_vocbase->_cursorRepository);
|
|
TRI_ASSERT(cursors != nullptr);
|
|
|
|
auto cursorId = static_cast<triagens::arango::CursorId>(triagens::basics::StringUtils::uint64(id));
|
|
bool busy;
|
|
auto cursor = cursors->find(cursorId, busy);
|
|
|
|
if (cursor == nullptr) {
|
|
if (busy) {
|
|
generateError(HttpResponse::responseCode(TRI_ERROR_CURSOR_BUSY), TRI_ERROR_CURSOR_BUSY);
|
|
}
|
|
else {
|
|
generateError(HttpResponse::responseCode(TRI_ERROR_CURSOR_NOT_FOUND), TRI_ERROR_CURSOR_NOT_FOUND);
|
|
}
|
|
return;
|
|
}
|
|
|
|
try {
|
|
_response = createResponse(HttpResponse::OK);
|
|
_response->setContentType("application/json; charset=utf-8");
|
|
|
|
_response->body().appendChar('{');
|
|
cursor->dump(_response->body());
|
|
_response->body().appendText(",\"error\":false,\"code\":");
|
|
_response->body().appendInteger(static_cast<uint32_t>(_response->responseCode()));
|
|
_response->body().appendChar('}');
|
|
|
|
cursors->release(cursor);
|
|
}
|
|
catch (triagens::basics::Exception const& ex) {
|
|
cursors->release(cursor);
|
|
|
|
generateError(HttpResponse::responseCode(ex.code()), ex.code(), ex.what());
|
|
}
|
|
catch (...) {
|
|
cursors->release(cursor);
|
|
|
|
generateError(HttpResponse::SERVER_ERROR, TRI_ERROR_INTERNAL);
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
/// @startDocuBlock JSF_post_api_cursor_delete
|
|
/// @brief dispose an existing cursor
|
|
///
|
|
/// @RESTHEADER{DELETE /_api/cursor/{cursor-identifier}, Delete cursor}
|
|
///
|
|
/// @RESTURLPARAMETERS
|
|
///
|
|
/// @RESTURLPARAM{cursor-identifier,string,required}
|
|
/// The id of the cursor
|
|
///
|
|
/// @RESTDESCRIPTION
|
|
/// Deletes the cursor and frees the resources associated with it.
|
|
///
|
|
/// The cursor will automatically be destroyed on the server when the client has
|
|
/// retrieved all documents from it. The client can also explicitly destroy the
|
|
/// cursor at any earlier time using an HTTP DELETE request. The cursor id must
|
|
/// be included as part of the URL.
|
|
///
|
|
/// Note: the server will also destroy abandoned cursors automatically after a
|
|
/// certain server-controlled timeout to avoid resource leakage.
|
|
///
|
|
/// @RESTRETURNCODES
|
|
///
|
|
/// @RESTRETURNCODE{202}
|
|
/// is returned if the server is aware of the cursor.
|
|
///
|
|
/// @RESTRETURNCODE{404}
|
|
/// is returned if the server is not aware of the cursor. It is also
|
|
/// returned if a cursor is used after it has been destroyed.
|
|
///
|
|
/// @EXAMPLES
|
|
///
|
|
/// @EXAMPLE_ARANGOSH_RUN{RestCursorDelete}
|
|
/// var url = "/_api/cursor";
|
|
/// var cn = "products";
|
|
/// db._drop(cn);
|
|
/// db._create(cn);
|
|
///
|
|
/// db.products.save({"hello1":"world1"});
|
|
/// db.products.save({"hello2":"world1"});
|
|
/// db.products.save({"hello3":"world1"});
|
|
/// db.products.save({"hello4":"world1"});
|
|
/// db.products.save({"hello5":"world1"});
|
|
///
|
|
/// var url = "/_api/cursor";
|
|
/// var body = {
|
|
/// query: "FOR p IN products LIMIT 5 RETURN p",
|
|
/// count: true,
|
|
/// batchSize: 2
|
|
/// };
|
|
/// var response = logCurlRequest('POST', url, JSON.stringify(body));
|
|
/// logJsonResponse(response);
|
|
/// var body = response.body.replace(/\\/g, '');
|
|
/// var _id = JSON.parse(body).id;
|
|
/// response = logCurlRequest('DELETE', url + '/' + _id);
|
|
///
|
|
/// assert(response.code === 202);
|
|
/// ~ db._drop(cn);
|
|
/// @END_EXAMPLE_ARANGOSH_RUN
|
|
/// @endDocuBlock
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
void RestCursorHandler::deleteCursor () {
|
|
std::vector<std::string> const& suffix = _request->suffix();
|
|
|
|
if (suffix.size() != 1) {
|
|
generateError(HttpResponse::BAD,
|
|
TRI_ERROR_HTTP_BAD_PARAMETER,
|
|
"expecting DELETE /_api/cursor/<cursor-id>");
|
|
return;
|
|
}
|
|
|
|
std::string const& id = suffix[0];
|
|
|
|
auto cursors = static_cast<triagens::arango::CursorRepository*>(_vocbase->_cursorRepository);
|
|
TRI_ASSERT(cursors != nullptr);
|
|
|
|
auto cursorId = static_cast<triagens::arango::CursorId>(triagens::basics::StringUtils::uint64(id));
|
|
bool found = cursors->remove(cursorId);
|
|
|
|
if (! found) {
|
|
generateError(HttpResponse::NOT_FOUND, TRI_ERROR_CURSOR_NOT_FOUND);
|
|
return;
|
|
}
|
|
|
|
_response = createResponse(HttpResponse::ACCEPTED);
|
|
_response->setContentType("application/json; charset=utf-8");
|
|
|
|
triagens::basics::Json json(triagens::basics::Json::Object);
|
|
json.set("id", triagens::basics::Json(id)); // id as a string!
|
|
json.set("error", triagens::basics::Json(false));
|
|
json.set("code", triagens::basics::Json(static_cast<double>(_response->responseCode())));
|
|
|
|
json.dump(_response->body());
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// --SECTION-- END-OF-FILE
|
|
// -----------------------------------------------------------------------------
|
|
|
|
// Local Variables:
|
|
// mode: outline-minor
|
|
// outline-regexp: "/// @brief\\|/// {@inheritDoc}\\|/// @page\\|// --SECTION--\\|/// @\\}"
|
|
// End:
|