12 KiB
////////////////////////////////////////////////////////////////////////////////
/// @startDocuBlock JSF_post_api_cursor
/// @brief create a cursor and return the first results
///
/// @RESTHEADER{POST /_api/cursor, Create cursor}
///
/// A JSON object describing the query and query parameters.
///
/// @RESTBODYPARAM{query,string,required,string}
/// contains the query string to be executed
///
/// @RESTBODYPARAM{count,boolean,optional,}
/// indicates whether the number of documents in the result set should be returned in
/// the "count" attribute of the result.
/// 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.
///
/// @RESTBODYPARAM{batchSize,integer,optional,int64}
/// maximum number of result documents to be transferred from
/// the server to the client in one roundtrip. If this attribute is
/// not set, a server-controlled default value will be used. A batchSize value of
/// 0 is disallowed.
///
/// @RESTBODYPARAM{ttl,integer,optional,int64}
/// The 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.
///
/// @RESTBODYPARAM{cache,boolean,optional,}
/// flag to determine whether the AQL query cache
/// shall be used. If set to false, then any query cache lookup will be skipped
/// for the query. If set to true, it will lead to the query cache being checked
/// for the query if the query cache mode is either on or demand.
///
/// @RESTBODYPARAM{bindVars,array,optional,object}
/// list of bind parameter objects.
///
/// @RESTBODYPARAM{options,object,optional,JSF_post_api_cursor_opts}
/// key/value object with extra options for the query.
///
/// @RESTSTRUCT{fullCount,JSF_post_api_cursor_opts,boolean,optional,}
/// 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.
///
/// @RESTSTRUCT{maxPlans,JSF_post_api_cursor_opts,integer,optional,int64}
/// limits the maximum number of plans that are created by the AQL query optimizer.
///
/// @RESTSTRUCT{optimizer.rules,JSF_post_api_cursor_opts,array,optional,string}
/// 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.
///
/// @RESTSTRUCT{profile,JSF_post_api_cursor_opts,boolean,optional,}
/// if set to true, then the additional query profiling information
/// will be returned in the extra.stats return attribute if the query result is not
/// served from the query cache.
///
/// @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.
///
/// @RESTRETURNCODES
///
/// @RESTRETURNCODE{201}
/// is returned if the result set can be created by the server.
///
/// @RESTREPLYBODY{error,boolean,required,}
/// A flag to indicate that an error occurred (false in this case)
///
/// @RESTREPLYBODY{code,integer,required,integer}
/// the HTTP status code
///
/// @RESTREPLYBODY{result,array,optional,}
/// an array of result documents (might be empty if query has no results)
///
/// @RESTREPLYBODY{hasMore,boolean,required,}
/// A boolean indicator whether there are more results
/// available for the cursor on the server
///
/// @RESTREPLYBODY{count,integer,optional,int64}
/// the total number of result documents available (only
/// available if the query was executed with the count attribute set)
///
/// @RESTREPLYBODY{id,string,required,string}
/// id of temporary cursor created on the server (optional, see above)
///
/// @RESTREPLYBODY{extra,object,optional,}
/// an optional JSON object with extra information about the query result
/// contained in its stats sub-attribute. For data-modification queries, the
/// extra.stats sub-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)
///
/// @RESTREPLYBODY{cached,boolean,required,}
/// a boolean flag indicating whether the query result was served
/// from the query cache or not. If the query result is served from the query
/// cache, the extra return attribute will not contain any stats sub-attribute
/// and no profile sub-attribute.
///
/// @RESTRETURNCODE{400}
/// is returned if the JSON representation is malformed or the query specification is
/// missing from the request.
///
/// 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:
///
/// @RESTREPLYBODY{error,boolean,required,}
/// boolean flag to indicate that an error occurred (true in this case)
///
/// @RESTREPLYBODY{code,integer,required,int64}
/// the HTTP status code
///
/// @RESTREPLYBODY{errorNum,integer,required,int64}
/// the server error number
///
/// @RESTREPLYBODY{errorMessage,string,required,string}
/// 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 here.
///
///
/// @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
///
/// Execute 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, body);
///
/// assert(response.code === 201);
///
/// logJsonResponse(response);
/// ~ db._drop(cn);
/// @END_EXAMPLE_ARANGOSH_RUN
///
/// Execute a query and extract a 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, body);
///
/// assert(response.code === 201);
///
/// logJsonResponse(response);
/// ~ db._drop(cn);
/// @END_EXAMPLE_ARANGOSH_RUN
///
/// Using the 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, 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, body);
///
/// assert(response.code === 201);
///
/// logJsonResponse(response);
/// @END_EXAMPLE_ARANGOSH_RUN
///
/// Execute a data-modification query and retrieve 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, 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
///
/// Execute 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, 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 query - 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
///
/// Bad query - 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, body);
///
/// assert(response.code === 404);
///
/// logJsonResponse(response);
/// @END_EXAMPLE_ARANGOSH_RUN
///
/// Bad query - Execute 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, body);
///
/// assert(response.code === 404);
///
/// logJsonResponse(response);
/// ~ db._drop(cn);
/// @END_EXAMPLE_ARANGOSH_RUN
///
/// @endDocuBlock
////////////////////////////////////////////////////////////////////////////////