diff --git a/CHANGELOG b/CHANGELOG index b8e5a8b539..24ac5b5da1 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -26,6 +26,8 @@ devel be used that has access to the `_system` database, in order to create the databases on restore. +* added index hints feature to AQL + * added "name" property for indices If a name is not specified on index creation, one will be auto-generated. diff --git a/Documentation/Books/AQL/Operations/For.md b/Documentation/Books/AQL/Operations/For.md index a32ac8bd82..7f739225f5 100644 --- a/Documentation/Books/AQL/Operations/For.md +++ b/Documentation/Books/AQL/Operations/For.md @@ -33,7 +33,7 @@ required that *expression* returns an array in all cases. The empty array is allowed, too. The current array element is made available for further processing in the variable specified by *variableName*. -``` +```js FOR u IN users RETURN u ``` @@ -52,7 +52,7 @@ placed in is closed. Another example that uses a statically declared array of values to iterate over: -``` +```js FOR year IN [ 2011, 2012, 2013 ] RETURN { "year" : year, "isLeapYear" : year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) } ``` @@ -61,7 +61,7 @@ Nesting of multiple *FOR* statements is allowed, too. When *FOR* statements are nested, a cross product of the array elements returned by the individual *FOR* statements will be created. -``` +```js FOR u IN users FOR l IN locations RETURN { "user" : u, "location" : l } @@ -72,3 +72,39 @@ In this example, there are two array iterations: an outer iteration over the arr traversed as many times as there are elements in the outer array. For each iteration, the current values of *users* and *locations* are made available for further processing in the variable *u* and *l*. + +## Options + +For collections and views, the `FOR` construct supports an optional `OPTIONS` +suffix to modify behavior. The general syntax is: + +```js +FOR variableName IN expression OPTIONS {option: value, ...} +``` + +### Index hints + +For collections, index hints are provided though this inline options mechanism. +Hints can be specified in two different formats. + +The first format option is the simplest, just a single index name. This should +be sufficient for many cases. Whenever there is a choice to potentially use an +index for this `FOR` loop, the optimizer will first check if the specified index +can be used. If so, it will use it, regardless of whether it would normally use +a different index. If it cannot use that index, then it will fall back to its +normal logic to select another index. If the optional `forceIndexHint: true` is +specified, then it will not fall back, and instead generate an error. + +```js +OPTIONS {indexHint: 'byName'[, forceIndexHint: ]} +``` + +The second is an array of index names, in order of preference. When specified +this way, the optimizer will behave much in the same way as above, but will +check the feasibility of each of the specified indices, in the order they are +given, falling back to its normal logic or failing only if none of the specified +indices are feasible. + +```js +OPTIONS {indexHint: ['byName', 'byColor'][, forceIndexHint: ]} +``` diff --git a/Documentation/Books/Manual/ReleaseNotes/NewFeatures35.md b/Documentation/Books/Manual/ReleaseNotes/NewFeatures35.md index f097ad77e7..4b02a748f2 100644 --- a/Documentation/Books/Manual/ReleaseNotes/NewFeatures35.md +++ b/Documentation/Books/Manual/ReleaseNotes/NewFeatures35.md @@ -262,6 +262,14 @@ name _cannot_ be changed after index creation. No two indices on the same collection may share the same name, but two indices on different collections may. +### Index Hints in AQL + +Users may now take advantage of the `indexHint` inline query option to override +the internal optimizer decision regarding which index to use to serve content +from a given collection. The index hint works with the named indices feature +above, making it easy to specify which index to use. + + Client tools ------------ diff --git a/arangod/Aql/Condition.cpp b/arangod/Aql/Condition.cpp index b553ebf0a3..f6dc7dbc6c 100644 --- a/arangod/Aql/Condition.cpp +++ b/arangod/Aql/Condition.cpp @@ -462,12 +462,14 @@ std::pair Condition::findIndexes(EnumerateCollectionNode const* node } if (_root == nullptr) { size_t dummy; - return std::make_pair(false, trx->getIndexForSortCondition(collectionName, sortCondition, reference, - itemsInIndex, usedIndexes, dummy)); + return std::make_pair( + false, trx->getIndexForSortCondition(collectionName, sortCondition, reference, itemsInIndex, + node->hint(), usedIndexes, dummy)); } return trx->getBestIndexHandlesForFilterCondition(collectionName, _ast, _root, - reference, sortCondition, itemsInIndex, + reference, sortCondition, + itemsInIndex, node->hint(), usedIndexes, _isSorted); } diff --git a/arangod/Aql/ExecutionNode.cpp b/arangod/Aql/ExecutionNode.cpp index f2f66741e1..123f58e83f 100644 --- a/arangod/Aql/ExecutionNode.cpp +++ b/arangod/Aql/ExecutionNode.cpp @@ -1275,7 +1275,8 @@ EnumerateCollectionNode::EnumerateCollectionNode(ExecutionPlan* plan, : ExecutionNode(plan, base), DocumentProducingNode(plan, base), CollectionAccessingNode(plan, base), - _random(base.get("random").getBoolean()) {} + _random(base.get("random").getBoolean()), + _hint(base) {} /// @brief toVelocyPack, for EnumerateCollectionNode void EnumerateCollectionNode::toVelocyPackHelper(VPackBuilder& builder, unsigned flags) const { @@ -1284,6 +1285,8 @@ void EnumerateCollectionNode::toVelocyPackHelper(VPackBuilder& builder, unsigned builder.add("random", VPackValue(_random)); + _hint.toVelocyPack(builder); + // add outvariable and projection DocumentProducingNode::toVelocyPack(builder); @@ -1325,7 +1328,7 @@ ExecutionNode* EnumerateCollectionNode::clone(ExecutionPlan* plan, bool withDepe } auto c = std::make_unique(plan, _id, _collection, - outVariable, _random); + outVariable, _random, _hint); c->projections(_projections); diff --git a/arangod/Aql/ExecutionNode.h b/arangod/Aql/ExecutionNode.h index b93224fe8e..c14a089986 100644 --- a/arangod/Aql/ExecutionNode.h +++ b/arangod/Aql/ExecutionNode.h @@ -56,6 +56,7 @@ #include "Aql/CostEstimate.h" #include "Aql/DocumentProducingNode.h" #include "Aql/Expression.h" +#include "Aql/IndexHint.h" #include "Aql/Variable.h" #include "Aql/WalkerWorker.h" #include "Aql/types.h" @@ -637,11 +638,12 @@ class EnumerateCollectionNode : public ExecutionNode, /// @brief constructor with a vocbase and a collection name public: EnumerateCollectionNode(ExecutionPlan* plan, size_t id, aql::Collection const* collection, - Variable const* outVariable, bool random) + Variable const* outVariable, bool random, IndexHint const& hint) : ExecutionNode(plan, id), DocumentProducingNode(outVariable), CollectionAccessingNode(collection), - _random(random) {} + _random(random), + _hint(hint) {} EnumerateCollectionNode(ExecutionPlan* plan, arangodb::velocypack::Slice const& base); @@ -675,9 +677,15 @@ class EnumerateCollectionNode : public ExecutionNode, /// @brief enable random iteration of documents in collection void setRandom() { _random = true; } + /// @brief user hint regarding which index ot use + IndexHint const& hint() const { return _hint; } + private: /// @brief whether or not we want random iteration bool _random; + + /// @brief a possible hint from the user regarding which index to use + IndexHint _hint; }; /// @brief class EnumerateListNode diff --git a/arangod/Aql/ExecutionPlan.cpp b/arangod/Aql/ExecutionPlan.cpp index 12c1fbb169..f31ae671a4 100644 --- a/arangod/Aql/ExecutionPlan.cpp +++ b/arangod/Aql/ExecutionPlan.cpp @@ -30,6 +30,7 @@ #include "Aql/ExecutionNode.h" #include "Aql/Expression.h" #include "Aql/Function.h" +#include "Aql/IndexHint.h" #include "Aql/ModificationNodes.h" #include "Aql/NodeFinder.h" #include "Aql/OptimizerRulesFeature.h" @@ -842,7 +843,8 @@ ExecutionNode* ExecutionPlan::fromNodeFor(ExecutionNode* previous, AstNode const auto variable = node->getMember(0); auto expression = node->getMember(1); - // TODO: process FOR options here if we want to use them later + auto options = node->getMember(2); + IndexHint hint(options); // fetch 1st operand (out variable name) TRI_ASSERT(variable->type == NODE_TYPE_VARIABLE); @@ -862,7 +864,7 @@ ExecutionNode* ExecutionPlan::fromNodeFor(ExecutionNode* previous, AstNode const THROW_ARANGO_EXCEPTION_MESSAGE(TRI_ERROR_INTERNAL, "no collection for EnumerateCollection"); } - en = registerNode(new EnumerateCollectionNode(this, nextId(), collection, v, false)); + en = registerNode(new EnumerateCollectionNode(this, nextId(), collection, v, false, hint)); #ifdef USE_IRESEARCH } else if (expression->type == NODE_TYPE_VIEW) { // second operand is a view diff --git a/arangod/Aql/IndexHint.cpp b/arangod/Aql/IndexHint.cpp new file mode 100644 index 0000000000..9021f59d7d --- /dev/null +++ b/arangod/Aql/IndexHint.cpp @@ -0,0 +1,196 @@ +//////////////////////////////////////////////////////////////////////////////// +/// DISCLAIMER +/// +/// Copyright 2014-2019 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 Dan Larkin-York +//////////////////////////////////////////////////////////////////////////////// + +#include "IndexHint.h" + +#include +#include + +#include "Aql/AstNode.h" +#include "Basics/StaticStrings.h" +#include "Basics/VelocyPackHelper.h" + +namespace { +std::string const TypeIllegal("illegal"); +std::string const TypeNone("none"); +std::string const TypeSimple("simple"); + +std::string const FieldContainer("indexHint"); +std::string const FieldForced("forced"); +std::string const FieldHint("hint"); +std::string const FieldType("type"); + +bool extractForced(arangodb::aql::AstNode const* node) { + using arangodb::aql::AstNode; + using arangodb::aql::AstNodeType; + using arangodb::aql::AstNodeValueType; + + bool forced = false; + + if (node->type == AstNodeType::NODE_TYPE_OBJECT) { + for (size_t i = 0; i < node->numMembers(); i++) { + AstNode const* child = node->getMember(i); + + if (child->type == AstNodeType::NODE_TYPE_OBJECT_ELEMENT) { + VPackStringRef name(child->getStringValue(), child->getStringLength()); + + if (name == arangodb::StaticStrings::IndexHintOptionForce) { + TRI_ASSERT(child->numMembers() > 0); + AstNode const* value = child->getMember(0); + + if (value->type == AstNodeType::NODE_TYPE_VALUE && + value->value.type == AstNodeValueType::VALUE_TYPE_BOOL) { + forced = value->value.value._bool; + } + } + } + } + } + + return forced; +} + +arangodb::aql::IndexHint::HintType fromTypeName(std::string const& typeName) { + if (::TypeSimple == typeName) { + return arangodb::aql::IndexHint::HintType::Simple; + } else if (::TypeNone == typeName) { + return arangodb::aql::IndexHint::HintType::None; + } + + return arangodb::aql::IndexHint::HintType::Illegal; +} +} // namespace + +namespace arangodb { +namespace aql { + +IndexHint::IndexHint() : _type{HintType::None}, _forced{false} {} + +IndexHint::IndexHint(AstNode const* node) + : _type{HintType::None}, _forced{::extractForced(node)} { + if (node->type == AstNodeType::NODE_TYPE_OBJECT) { + for (size_t i = 0; i < node->numMembers(); i++) { + AstNode const* child = node->getMember(i); + + if (child->type == AstNodeType::NODE_TYPE_OBJECT_ELEMENT) { + VPackStringRef name(child->getStringValue(), child->getStringLength()); + + if (name == StaticStrings::IndexHintOption) { + TRI_ASSERT(child->numMembers() > 0); + AstNode const* value = child->getMember(0); + + if (value->type == AstNodeType::NODE_TYPE_VALUE && + value->value.type == AstNodeValueType::VALUE_TYPE_STRING) { + _type = HintType::Simple; + _hint.simple.emplace_back(value->getStringValue(), value->getStringLength()); + } + + if (value->type == AstNodeType::NODE_TYPE_ARRAY) { + _type = HintType::Simple; + for (size_t j = 0; j < value->numMembers(); j++) { + AstNode const* member = value->getMember(j); + if (member->type == AstNodeType::NODE_TYPE_VALUE && + member->value.type == AstNodeValueType::VALUE_TYPE_STRING) { + _hint.simple.emplace_back(member->getStringValue(), + member->getStringLength()); + } + } + } + } + } + } + } +} + +IndexHint::IndexHint(VPackSlice const& slice) + : _type{::fromTypeName( + basics::VelocyPackHelper::getStringValue(slice.get(::FieldContainer), + ::FieldType, ""))}, + _forced{basics::VelocyPackHelper::getBooleanValue(slice.get(::FieldContainer), + ::FieldForced, false)} { + if (_type != HintType::Illegal && _type != HintType::None) { + VPackSlice container = slice.get(::FieldContainer); + TRI_ASSERT(container.isObject()); + + if (_type == HintType::Simple) { + VPackSlice hintSlice = container.get(::FieldHint); + TRI_ASSERT(hintSlice.isArray()); + for (VPackSlice index : VPackArrayIterator(hintSlice)) { + TRI_ASSERT(index.isString()); + _hint.simple.emplace_back(index.copyString()); + } + } + } +} + +IndexHint::HintType IndexHint::type() const { return _type; } + +bool IndexHint::isForced() const { return _forced; } + +std::vector const& IndexHint::hint() const { + TRI_ASSERT(_type == HintType::Simple); + return _hint.simple; +} + +std::string IndexHint::typeName() const { + switch (_type) { + case HintType::Illegal: + return ::TypeIllegal; + case HintType::None: + return ::TypeNone; + case HintType::Simple: + return ::TypeSimple; + } + + return ::TypeIllegal; +} + +void IndexHint::toVelocyPack(VPackBuilder& builder) const { + TRI_ASSERT(builder.isOpenObject()); + VPackObjectBuilder guard(&builder, ::FieldContainer); + builder.add(::FieldForced, VPackValue(_forced)); + builder.add(::FieldType, VPackValue(typeName())); + if (_type == HintType::Simple) { + VPackArrayBuilder hintGuard(&builder, ::FieldHint); + for (std::string const& index : _hint.simple) { + builder.add(VPackValue(index)); + } + } +} + +std::string IndexHint::toString() const { + VPackBuilder builder; + { + VPackObjectBuilder guard(&builder); + toVelocyPack(builder); + } + return builder.slice().toJson(); +} + +std::ostream& operator<<(std::ostream& stream, arangodb::aql::IndexHint const& hint) { + stream << hint.toString(); + return stream; +} + +} // namespace aql +} // namespace arangodb diff --git a/arangod/Aql/IndexHint.h b/arangod/Aql/IndexHint.h new file mode 100644 index 0000000000..69acce300b --- /dev/null +++ b/arangod/Aql/IndexHint.h @@ -0,0 +1,74 @@ +//////////////////////////////////////////////////////////////////////////////// +/// DISCLAIMER +/// +/// Copyright 2014-2019 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 Dan Larkin-York +//////////////////////////////////////////////////////////////////////////////// + +#ifndef ARANGOD_AQL_INDEX_HINT_H +#define ARANGOD_AQL_INDEX_HINT_H 1 + +#include + +#include +#include +#include + +#include "Aql/AstNode.h" + +namespace arangodb { +namespace aql { + +/// @brief container for index hint information +class IndexHint { + public: + enum HintType : uint8_t { Illegal, None, Simple }; + + public: + explicit IndexHint(); + explicit IndexHint(AstNode const* node); + explicit IndexHint(VPackSlice const& slice); + + public: + HintType type() const; + bool isForced() const; + std::vector const& hint() const; + + void toVelocyPack(VPackBuilder& builder) const; + std::string typeName() const; + std::string toString() const; + + private: + HintType _type; + bool const _forced; + + // actual hint is a recursive structure, with the data type determined by the + // _type above; in the case of a nested IndexHint, the value of isForced() is + // inherited + struct HintData { + std::vector simple; + } _hint; +}; + +std::ostream& operator<<(std::ostream& stream, arangodb::aql::IndexHint const& hint); + +} // namespace aql +} // namespace arangodb + +#endif diff --git a/arangod/Aql/OptimizerRules.cpp b/arangod/Aql/OptimizerRules.cpp index 15446c7eaa..4613922e4e 100644 --- a/arangod/Aql/OptimizerRules.cpp +++ b/arangod/Aql/OptimizerRules.cpp @@ -2880,11 +2880,10 @@ struct SortToIndexNode final : public WalkerWorker { std::vector usedIndexes; auto trx = _plan->getAst()->query()->trx(); size_t coveredAttributes = 0; - bool canBeUsed = - trx->getIndexForSortCondition(enumerateCollectionNode->collection()->name(), - &sortCondition, outVariable, - enumerateCollectionNode->collection()->count(trx), - usedIndexes, coveredAttributes); + bool canBeUsed = trx->getIndexForSortCondition( + enumerateCollectionNode->collection()->name(), &sortCondition, + outVariable, enumerateCollectionNode->collection()->count(trx), + enumerateCollectionNode->hint(), usedIndexes, coveredAttributes); if (canBeUsed) { // If this bit is set, then usedIndexes has length exactly one // and contains the best index found. diff --git a/arangod/Aql/OptimizerRulesReplaceFunctions.cpp b/arangod/Aql/OptimizerRulesReplaceFunctions.cpp index cf912da7f9..33a275a5fe 100644 --- a/arangod/Aql/OptimizerRulesReplaceFunctions.cpp +++ b/arangod/Aql/OptimizerRulesReplaceFunctions.cpp @@ -24,6 +24,7 @@ #include "Aql/ExecutionNode.h" #include "Aql/ExecutionPlan.h" #include "Aql/Function.h" +#include "Aql/IndexHint.h" #include "Aql/IndexNode.h" #include "Aql/Optimizer.h" #include "Aql/Query.h" @@ -266,7 +267,7 @@ AstNode* replaceNearOrWithin(AstNode* funAstNode, ExecutionNode* calcNode, ExecutionNode* eEnumerate = plan->registerNode( // link output of index with the return node new EnumerateCollectionNode(plan, plan->nextId(), aqlCollection, - enumerateOutVariable, false)); + enumerateOutVariable, false, IndexHint())); //// build sort condition - DISTANCE(d.lat, d.long, param.lat, param.lon) auto* docRef = ast->createNodeReference(enumerateOutVariable); diff --git a/arangod/CMakeLists.txt b/arangod/CMakeLists.txt index b68c531f23..28204b87f3 100644 --- a/arangod/CMakeLists.txt +++ b/arangod/CMakeLists.txt @@ -227,6 +227,7 @@ SET(ARANGOD_SOURCES Aql/InAndOutRowExpressionContext.cpp Aql/IdExecutor.cpp Aql/IndexExecutor.cpp + Aql/IndexHint.cpp Aql/IndexNode.cpp Aql/InputAqlItemRow.cpp Aql/LimitExecutor.cpp diff --git a/arangod/Cluster/ClusterMethods.cpp b/arangod/Cluster/ClusterMethods.cpp index 3b3f2e9d02..f3ff51aaae 100644 --- a/arangod/Cluster/ClusterMethods.cpp +++ b/arangod/Cluster/ClusterMethods.cpp @@ -31,6 +31,7 @@ #include "Basics/tri-strings.h" #include "Cluster/ClusterComm.h" #include "Cluster/ClusterInfo.h" +#include "ClusterMethods.h" #include "Graph/Traverser.h" #include "Indexes/Index.h" #include "RestServer/TtlFeature.h" diff --git a/arangod/Graph/BaseOptions.cpp b/arangod/Graph/BaseOptions.cpp index 4b8170a076..5ab8689d71 100644 --- a/arangod/Graph/BaseOptions.cpp +++ b/arangod/Graph/BaseOptions.cpp @@ -248,9 +248,10 @@ void BaseOptions::injectLookupInfoInList(std::vector& list, aql::AstNode* condition) { LookupInfo info; info.indexCondition = condition->clone(plan->getAst()); - bool res = _trx->getBestIndexHandleForFilterCondition(collectionName, - info.indexCondition, _tmpVar, - 1000, info.idxHandles[0]); + bool res = + _trx->getBestIndexHandleForFilterCondition(collectionName, info.indexCondition, + _tmpVar, 1000, aql::IndexHint(), + info.idxHandles[0]); // Right now we have an enforced edge index which should always fit. if (!res) { THROW_ARANGO_EXCEPTION_MESSAGE(TRI_ERROR_INTERNAL, diff --git a/arangod/Graph/EdgeCollectionInfo.cpp b/arangod/Graph/EdgeCollectionInfo.cpp index daa5b25b2d..cce93a8bdf 100644 --- a/arangod/Graph/EdgeCollectionInfo.cpp +++ b/arangod/Graph/EdgeCollectionInfo.cpp @@ -46,21 +46,25 @@ EdgeCollectionInfo::EdgeCollectionInfo(transaction::Methods* trx, auto var = _searchBuilder.getVariable(); if (_dir == TRI_EDGE_OUT) { auto cond = _searchBuilder.getOutboundCondition(); - bool worked = _trx->getBestIndexHandleForFilterCondition(_collectionName, cond, - var, 1000, _forwardIndexId); + bool worked = + _trx->getBestIndexHandleForFilterCondition(_collectionName, cond, var, 1000, + aql::IndexHint(), _forwardIndexId); TRI_ASSERT(worked); // We always have an edge Index cond = _searchBuilder.getInboundCondition(); worked = _trx->getBestIndexHandleForFilterCondition(_collectionName, cond, var, - 1000, _backwardIndexId); + 1000, aql::IndexHint(), + _backwardIndexId); TRI_ASSERT(worked); // We always have an edge Index } else { auto cond = _searchBuilder.getInboundCondition(); - bool worked = _trx->getBestIndexHandleForFilterCondition(_collectionName, cond, - var, 1000, _forwardIndexId); + bool worked = + _trx->getBestIndexHandleForFilterCondition(_collectionName, cond, var, 1000, + aql::IndexHint(), _forwardIndexId); TRI_ASSERT(worked); // We always have an edge Index cond = _searchBuilder.getOutboundCondition(); worked = _trx->getBestIndexHandleForFilterCondition(_collectionName, cond, var, - 1000, _backwardIndexId); + 1000, aql::IndexHint(), + _backwardIndexId); TRI_ASSERT(worked); // We always have an edge Index } } diff --git a/arangod/RestHandler/RestEdgesHandler.cpp b/arangod/RestHandler/RestEdgesHandler.cpp index cfbdf2501a..213b19b3e7 100644 --- a/arangod/RestHandler/RestEdgesHandler.cpp +++ b/arangod/RestHandler/RestEdgesHandler.cpp @@ -67,8 +67,9 @@ void RestEdgesHandler::readCursor(aql::AstNode* condition, aql::Variable const* SingleCollectionTransaction& trx, std::function const& cb) { transaction::Methods::IndexHandle indexId; - bool foundIdx = trx.getBestIndexHandleForFilterCondition(collectionName, condition, - var, 1000, indexId); + bool foundIdx = + trx.getBestIndexHandleForFilterCondition(collectionName, condition, var, + 1000, aql::IndexHint(), indexId); if (!foundIdx) { // Right now we enforce an edge index that can exactly! work on this // condition. So it is impossible to not find an index. diff --git a/arangod/Transaction/Methods.cpp b/arangod/Transaction/Methods.cpp index b533af5f44..4ebf649ad3 100644 --- a/arangod/Transaction/Methods.cpp +++ b/arangod/Transaction/Methods.cpp @@ -576,14 +576,16 @@ std::pair transaction::Methods::findIndexHandleForAndNode( std::vector> const& indexes, arangodb::aql::AstNode* node, arangodb::aql::Variable const* reference, arangodb::aql::SortCondition const* sortCondition, size_t itemsInCollection, - std::vector& usedIndexes, + aql::IndexHint const& hint, std::vector& usedIndexes, arangodb::aql::AstNode*& specializedCondition, bool& isSparse) const { std::shared_ptr bestIndex; double bestCost = 0.0; bool bestSupportsFilter = false; bool bestSupportsSort = false; - for (auto const& idx : indexes) { + auto considerIndex = [&bestIndex, &bestCost, &bestSupportsFilter, &bestSupportsSort, + &indexes, node, reference, itemsInCollection, + sortCondition](std::shared_ptr const& idx) -> void { double filterCost = 0.0; double sortCost = 0.0; size_t itemsInIndex = itemsInCollection; @@ -636,7 +638,7 @@ std::pair transaction::Methods::findIndexHandleForAndNode( } if (!supportsFilter && !supportsSort) { - continue; + return; } double totalCost = filterCost; @@ -666,6 +668,38 @@ std::pair transaction::Methods::findIndexHandleForAndNode( bestSupportsFilter = supportsFilter; bestSupportsSort = supportsSort; } + }; + + if (hint.type() == aql::IndexHint::HintType::Simple) { + std::vector const& hintedIndices = hint.hint(); + for (std::string const& hinted : hintedIndices) { + std::shared_ptr matched; + for (std::shared_ptr const& idx : indexes) { + if (idx->name() == hinted) { + matched = idx; + break; + } + } + + if (matched != nullptr) { + considerIndex(matched); + if (bestIndex != nullptr) { + break; + } + } + } + + if (hint.isForced() && bestIndex == nullptr) { + THROW_ARANGO_EXCEPTION_MESSAGE( + TRI_ERROR_QUERY_FORCED_INDEX_HINT_UNUSABLE, + "could not use index hint to serve query; " + hint.toString()); + } + } + + if (bestIndex == nullptr) { + for (auto const& idx : indexes) { + considerIndex(idx); + } } if (bestIndex == nullptr) { @@ -681,13 +715,14 @@ std::pair transaction::Methods::findIndexHandleForAndNode( } bool transaction::Methods::findIndexHandleForAndNode( - std::vector> const& indexes, - arangodb::aql::AstNode*& node, arangodb::aql::Variable const* reference, - size_t itemsInCollection, transaction::Methods::IndexHandle& usedIndex) const { + std::vector> const& indexes, arangodb::aql::AstNode*& node, + arangodb::aql::Variable const* reference, size_t itemsInCollection, + aql::IndexHint const& hint, transaction::Methods::IndexHandle& usedIndex) const { std::shared_ptr bestIndex; double bestCost = 0.0; - for (auto const& idx : indexes) { + auto considerIndex = [&bestIndex, &bestCost, itemsInCollection, &indexes, &node, + reference](std::shared_ptr const& idx) -> void { size_t itemsInIndex = itemsInCollection; // check if the index supports the filter expression @@ -709,7 +744,7 @@ bool transaction::Methods::findIndexHandleForAndNode( << ", node: " << node; if (!supportsFilter) { - continue; + return; } // index supports the filter condition @@ -721,6 +756,38 @@ bool transaction::Methods::findIndexHandleForAndNode( bestIndex = idx; bestCost = estimatedCost; } + }; + + if (hint.type() == aql::IndexHint::HintType::Simple) { + std::vector const& hintedIndices = hint.hint(); + for (std::string const& hinted : hintedIndices) { + std::shared_ptr matched; + for (std::shared_ptr const& idx : indexes) { + if (idx->name() == hinted) { + matched = idx; + break; + } + } + + if (matched != nullptr) { + considerIndex(matched); + if (bestIndex != nullptr) { + break; + } + } + } + + if (hint.isForced() && bestIndex == nullptr) { + THROW_ARANGO_EXCEPTION_MESSAGE( + TRI_ERROR_QUERY_FORCED_INDEX_HINT_UNUSABLE, + "could not use index hint to serve query; " + hint.toString()); + } + } + + if (bestIndex == nullptr) { + for (auto const& idx : indexes) { + considerIndex(idx); + } } if (bestIndex == nullptr) { @@ -2755,7 +2822,7 @@ std::pair transaction::Methods::getBestIndexHandlesForFilterConditio std::string const& collectionName, arangodb::aql::Ast* ast, arangodb::aql::AstNode* root, arangodb::aql::Variable const* reference, arangodb::aql::SortCondition const* sortCondition, size_t itemsInCollection, - std::vector& usedIndexes, bool& isSorted) { + aql::IndexHint const& hint, std::vector& usedIndexes, bool& isSorted) { // We can only start after DNF transformation TRI_ASSERT(root->type == arangodb::aql::AstNodeType::NODE_TYPE_OPERATOR_NARY_OR); auto indexes = indexesForCollection(collectionName); @@ -2771,7 +2838,7 @@ std::pair transaction::Methods::getBestIndexHandlesForFilterConditio auto node = root->getMemberUnchecked(i); arangodb::aql::AstNode* specializedCondition = nullptr; auto canUseIndex = findIndexHandleForAndNode(indexes, node, reference, sortCondition, - itemsInCollection, usedIndexes, + itemsInCollection, hint, usedIndexes, specializedCondition, isSparse); if (canUseIndex.second && !canUseIndex.first) { @@ -2817,7 +2884,7 @@ std::pair transaction::Methods::getBestIndexHandlesForFilterConditio bool transaction::Methods::getBestIndexHandleForFilterCondition( std::string const& collectionName, arangodb::aql::AstNode*& node, arangodb::aql::Variable const* reference, size_t itemsInCollection, - IndexHandle& usedIndex) { + aql::IndexHint const& hint, IndexHandle& usedIndex) { // We can only start after DNF transformation and only a single AND TRI_ASSERT(node->type == arangodb::aql::AstNodeType::NODE_TYPE_OPERATOR_NARY_AND); if (node->numMembers() == 0) { @@ -2829,7 +2896,8 @@ bool transaction::Methods::getBestIndexHandleForFilterCondition( // Const cast is save here. Giving computeSpecialization == false // Makes sure node is NOT modified. - return findIndexHandleForAndNode(indexes, node, reference, itemsInCollection, usedIndex); + return findIndexHandleForAndNode(indexes, node, reference, itemsInCollection, + hint, usedIndex); } /// @brief Checks if the index supports the filter condition. @@ -2872,20 +2940,20 @@ std::vector> transaction::Methods:: bool transaction::Methods::getIndexForSortCondition( std::string const& collectionName, arangodb::aql::SortCondition const* sortCondition, arangodb::aql::Variable const* reference, size_t itemsInIndex, - std::vector& usedIndexes, size_t& coveredAttributes) { + aql::IndexHint const& hint, std::vector& usedIndexes, + size_t& coveredAttributes) { // We do not have a condition. But we have a sort! if (!sortCondition->isEmpty() && sortCondition->isOnlyAttributeAccess() && sortCondition->isUnidirectional()) { double bestCost = 0.0; std::shared_ptr bestIndex; - auto indexes = indexesForCollection(collectionName); - - for (auto const& idx : indexes) { + auto considerIndex = [reference, sortCondition, itemsInIndex, &bestCost, &bestIndex, + &coveredAttributes](std::shared_ptr const& idx) -> void { if (idx->sparse()) { // a sparse index may exclude some documents, so it can't be used to // get a sorted view of the ENTIRE collection - continue; + return; } double sortCost = 0.0; size_t covered = 0; @@ -2897,6 +2965,40 @@ bool transaction::Methods::getIndexForSortCondition( coveredAttributes = covered; } } + }; + + auto indexes = indexesForCollection(collectionName); + + if (hint.type() == aql::IndexHint::HintType::Simple) { + std::vector const& hintedIndices = hint.hint(); + for (std::string const& hinted : hintedIndices) { + std::shared_ptr matched; + for (std::shared_ptr const& idx : indexes) { + if (idx->name() == hinted) { + matched = idx; + break; + } + } + + if (matched != nullptr) { + considerIndex(matched); + if (bestIndex != nullptr) { + break; + } + } + } + + if (hint.isForced() && bestIndex == nullptr) { + THROW_ARANGO_EXCEPTION_MESSAGE( + TRI_ERROR_QUERY_FORCED_INDEX_HINT_UNUSABLE, + "could not use index hint to serve query; " + hint.toString()); + } + } + + if (bestIndex == nullptr) { + for (auto const& idx : indexes) { + considerIndex(idx); + } } if (bestIndex != nullptr) { diff --git a/arangod/Transaction/Methods.h b/arangod/Transaction/Methods.h index fea5f475bc..fa258e48f9 100644 --- a/arangod/Transaction/Methods.h +++ b/arangod/Transaction/Methods.h @@ -24,6 +24,7 @@ #ifndef ARANGOD_TRANSACTION_METHODS_H #define ARANGOD_TRANSACTION_METHODS_H 1 +#include "Aql/IndexHint.h" #include "Basics/Common.h" #include "Basics/Exceptions.h" #include "Basics/Result.h" @@ -331,7 +332,7 @@ class Methods { ENTERPRISE_VIRT std::pair getBestIndexHandlesForFilterCondition( std::string const&, arangodb::aql::Ast*, arangodb::aql::AstNode*, arangodb::aql::Variable const*, arangodb::aql::SortCondition const*, - size_t, std::vector&, bool&); + size_t, aql::IndexHint const&, std::vector&, bool&); /// @brief Gets the best fitting index for one specific condition. /// Difference to IndexHandles: Condition is only one NARY_AND @@ -339,10 +340,9 @@ class Methods { /// Returns false if no index could be found. /// If it returned true, the AstNode contains the specialized condition - ENTERPRISE_VIRT bool getBestIndexHandleForFilterCondition(std::string const&, - arangodb::aql::AstNode*&, - arangodb::aql::Variable const*, - size_t, IndexHandle&); + ENTERPRISE_VIRT bool getBestIndexHandleForFilterCondition( + std::string const&, arangodb::aql::AstNode*&, + arangodb::aql::Variable const*, size_t, aql::IndexHint const&, IndexHandle&); /// @brief Checks if the index supports the filter condition. /// note: the caller must have read-locked the underlying collection when @@ -362,7 +362,8 @@ class Methods { ENTERPRISE_VIRT bool getIndexForSortCondition(std::string const&, arangodb::aql::SortCondition const*, arangodb::aql::Variable const*, - size_t, std::vector&, + size_t, aql::IndexHint const&, + std::vector&, size_t& coveredAttributes); /// @brief factory for IndexIterator objects from AQL @@ -562,13 +563,14 @@ class Methods { std::vector> const& indexes, arangodb::aql::AstNode* node, arangodb::aql::Variable const* reference, arangodb::aql::SortCondition const* sortCondition, size_t itemsInCollection, - std::vector& usedIndexes, + aql::IndexHint const& hint, std::vector& usedIndexes, arangodb::aql::AstNode*& specializedCondition, bool& isSparse) const; /// @brief findIndexHandleForAndNode, Shorthand which does not support Sort bool findIndexHandleForAndNode(std::vector> const& indexes, arangodb::aql::AstNode*& node, - arangodb::aql::Variable const* reference, size_t itemsInCollection, + arangodb::aql::Variable const* reference, + size_t itemsInCollection, aql::IndexHint const& hint, transaction::Methods::IndexHandle& usedIndex) const; /// @brief Get one index by id for a collection name, coordinator case diff --git a/js/common/bootstrap/errors.js b/js/common/bootstrap/errors.js index 45d0b3ba3d..c0601dc4dd 100644 --- a/js/common/bootstrap/errors.js +++ b/js/common/bootstrap/errors.js @@ -215,6 +215,7 @@ "ERROR_QUERY_INVALID_AGGREGATE_EXPRESSION" : { "code" : 1574, "message" : "invalid aggregate expression" }, "ERROR_QUERY_COMPILE_TIME_OPTIONS" : { "code" : 1575, "message" : "query options must be readable at query compile time" }, "ERROR_QUERY_EXCEPTION_OPTIONS" : { "code" : 1576, "message" : "query options expected" }, + "ERROR_QUERY_FORCED_INDEX_HINT_UNUSABLE" : { "code" : 1577, "message" : "could not use forced index hint" }, "ERROR_QUERY_DISALLOWED_DYNAMIC_CALL" : { "code" : 1578, "message" : "disallowed dynamic call to '%s'" }, "ERROR_QUERY_ACCESS_AFTER_MODIFICATION" : { "code" : 1579, "message" : "access after data-modification by %s" }, "ERROR_QUERY_FUNCTION_INVALID_NAME" : { "code" : 1580, "message" : "invalid user function name" }, diff --git a/js/common/modules/@arangodb/aql/explainer.js b/js/common/modules/@arangodb/aql/explainer.js index 3300217d13..2805a2afa2 100644 --- a/js/common/modules/@arangodb/aql/explainer.js +++ b/js/common/modules/@arangodb/aql/explainer.js @@ -294,6 +294,7 @@ function printIndexes(indexes) { var maxCollectionLen = String('Collection').length; var maxUniqueLen = String('Unique').length; var maxSparseLen = String('Sparse').length; + var maxNameLen = String('Name').length; var maxTypeLen = String('Type').length; var maxSelectivityLen = String('Selectivity').length; var maxFieldsLen = String('Fields').length; @@ -302,6 +303,10 @@ function printIndexes(indexes) { if (l > maxIdLen) { maxIdLen = l; } + l = index.name.length; + if (l > maxNameLen) { + maxNameLen = l; + } l = index.type.length; if (l > maxTypeLen) { maxTypeLen = l; @@ -316,6 +321,7 @@ function printIndexes(indexes) { } }); var line = ' ' + pad(1 + maxIdLen - String('By').length) + header('By') + ' ' + + header('Name') + pad(1 + maxNameLen - 'Name'.length) + ' ' + header('Type') + pad(1 + maxTypeLen - 'Type'.length) + ' ' + header('Collection') + pad(1 + maxCollectionLen - 'Collection'.length) + ' ' + header('Unique') + pad(1 + maxUniqueLen - 'Unique'.length) + ' ' + @@ -344,6 +350,7 @@ function printIndexes(indexes) { ); line = ' ' + pad(1 + maxIdLen - String(indexes[i].node).length) + variable(String(indexes[i].node)) + ' ' + + collection(indexes[i].name) + pad(1 + maxNameLen - indexes[i].name.length) + ' ' + keyword(indexes[i].type) + pad(1 + maxTypeLen - indexes[i].type.length) + ' ' + collection(indexes[i].collection) + pad(1 + maxCollectionLen - indexes[i].collection.length) + ' ' + value(uniqueness) + pad(1 + maxUniqueLen - uniqueness.length) + ' ' + diff --git a/lib/Basics/StaticStrings.cpp b/lib/Basics/StaticStrings.cpp index f552212efe..53123954a0 100644 --- a/lib/Basics/StaticStrings.cpp +++ b/lib/Basics/StaticStrings.cpp @@ -107,6 +107,17 @@ std::string const StaticStrings::IndexNameEdgeTo("edge_to"); std::string const StaticStrings::IndexNameInaccessible("inaccessible"); std::string const StaticStrings::IndexNamePrimary("primary"); +// index hint strings +std::string const StaticStrings::IndexHintAny("any"); +std::string const StaticStrings::IndexHintCollection("collection"); +std::string const StaticStrings::IndexHintHint("hint"); +std::string const StaticStrings::IndexHintDepth("depth"); +std::string const StaticStrings::IndexHintInbound("inbound"); +std::string const StaticStrings::IndexHintOption("indexHint"); +std::string const StaticStrings::IndexHintOptionForce("forceIndexHint"); +std::string const StaticStrings::IndexHintOutbound("outbound"); +std::string const StaticStrings::IndexHintWildcard("*"); + // HTTP headers std::string const StaticStrings::Accept("accept"); std::string const StaticStrings::AcceptEncoding("accept-encoding"); diff --git a/lib/Basics/StaticStrings.h b/lib/Basics/StaticStrings.h index 23b4aa23a2..1fbff11f2e 100644 --- a/lib/Basics/StaticStrings.h +++ b/lib/Basics/StaticStrings.h @@ -106,6 +106,17 @@ class StaticStrings { static std::string const IndexNameInaccessible; static std::string const IndexNamePrimary; + // index hint strings + static std::string const IndexHintAny; + static std::string const IndexHintCollection; + static std::string const IndexHintHint; + static std::string const IndexHintDepth; + static std::string const IndexHintInbound; + static std::string const IndexHintOption; + static std::string const IndexHintOptionForce; + static std::string const IndexHintOutbound; + static std::string const IndexHintWildcard; + // HTTP headers static std::string const Accept; static std::string const AcceptEncoding; diff --git a/lib/Basics/errors.dat b/lib/Basics/errors.dat index e84dd93e8b..3f7b013d5c 100755 --- a/lib/Basics/errors.dat +++ b/lib/Basics/errors.dat @@ -258,6 +258,7 @@ ERROR_QUERY_MULTI_MODIFY,1573,"multi-modify query","Will be raised when an AQL q ERROR_QUERY_INVALID_AGGREGATE_EXPRESSION,1574,"invalid aggregate expression","Will be raised when an AQL query contains an invalid aggregate expression." ERROR_QUERY_COMPILE_TIME_OPTIONS,1575,"query options must be readable at query compile time","Will be raised when an AQL data-modification query contains options that cannot be figured out at query compile time." ERROR_QUERY_EXCEPTION_OPTIONS,1576,"query options expected","Will be raised when an AQL data-modification query contains an invalid options specification." +ERROR_QUERY_FORCED_INDEX_HINT_UNUSABLE,1577,"could not use forced index hint","Will be raised when forceIndexHint is specified, and the hint cannot be used to serve the query." ERROR_QUERY_DISALLOWED_DYNAMIC_CALL,1578,"disallowed dynamic call to '%s'","Will be raised when a dynamic function call is made to a function that cannot be called dynamically." ERROR_QUERY_ACCESS_AFTER_MODIFICATION,1579,"access after data-modification by %s","Will be raised when collection data are accessed after a data-modification operation." diff --git a/lib/Basics/voc-errors.cpp b/lib/Basics/voc-errors.cpp index d143f32736..ade6748b2a 100644 --- a/lib/Basics/voc-errors.cpp +++ b/lib/Basics/voc-errors.cpp @@ -214,6 +214,7 @@ void TRI_InitializeErrorMessages() { REG_ERROR(ERROR_QUERY_INVALID_AGGREGATE_EXPRESSION, "invalid aggregate expression"); REG_ERROR(ERROR_QUERY_COMPILE_TIME_OPTIONS, "query options must be readable at query compile time"); REG_ERROR(ERROR_QUERY_EXCEPTION_OPTIONS, "query options expected"); + REG_ERROR(ERROR_QUERY_FORCED_INDEX_HINT_UNUSABLE, "could not use forced index hint"); REG_ERROR(ERROR_QUERY_DISALLOWED_DYNAMIC_CALL, "disallowed dynamic call to '%s'"); REG_ERROR(ERROR_QUERY_ACCESS_AFTER_MODIFICATION, "access after data-modification by %s"); REG_ERROR(ERROR_QUERY_FUNCTION_INVALID_NAME, "invalid user function name"); diff --git a/lib/Basics/voc-errors.h b/lib/Basics/voc-errors.h index fae732a449..816d519ecc 100644 --- a/lib/Basics/voc-errors.h +++ b/lib/Basics/voc-errors.h @@ -1148,6 +1148,12 @@ constexpr int TRI_ERROR_QUERY_COMPILE_TIME_OPTIONS /// options specification. constexpr int TRI_ERROR_QUERY_EXCEPTION_OPTIONS = 1576; +/// 1577: ERROR_QUERY_FORCED_INDEX_HINT_UNUSABLE +/// "could not use forced index hint" +/// Will be raised when forceIndexHint is specified, and the hint cannot be +/// used to serve the query. +constexpr int TRI_ERROR_QUERY_FORCED_INDEX_HINT_UNUSABLE = 1577; + /// 1578: ERROR_QUERY_DISALLOWED_DYNAMIC_CALL /// "disallowed dynamic call to '%s'" /// Will be raised when a dynamic function call is made to a function that diff --git a/tests/js/server/aql/aql-index-hints.js b/tests/js/server/aql/aql-index-hints.js new file mode 100644 index 0000000000..5122a5ab97 --- /dev/null +++ b/tests/js/server/aql/aql-index-hints.js @@ -0,0 +1,383 @@ +/*jshint globalstrict:false, strict:false, maxlen: 500 */ +/*global fail, assertEqual, AQL_EXPLAIN */ + +//////////////////////////////////////////////////////////////////////////////// +/// @brief tests for Ahuacatl, skiplist index queries +/// +/// @file +/// +/// DISCLAIMER +/// +/// Copyright 2010-2016 ArangoDB 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 Michael Hackstein +/// @author Copyright 2016, ArangoDB GmbH, Cologne, Germany +//////////////////////////////////////////////////////////////////////////////// + +var internal = require("internal"); +var db = internal.db; +var jsunity = require("jsunity"); +var errors = internal.errors; + +//////////////////////////////////////////////////////////////////////////////// +/// @brief test suite +//////////////////////////////////////////////////////////////////////////////// + +function ahuacatlSkiplistOverlappingTestSuite () { + const getIndexNames = function (query) { + return AQL_EXPLAIN(query, {}, { optimizer: { rules: [ "-all", "+use-indexes" ] } }) + .plan.nodes.filter(node => (node.type === 'IndexNode')) + .map(node => node.indexes.map(index => index.name)); + }; + + const cn = 'UnitTestsIndexHints'; + let collection; + let defaultEqualityIndex; + let alternateEqualityIndex; + let defaultSortingIndex; + let alternateSortingIndex; + + return { + +//////////////////////////////////////////////////////////////////////////////// +/// @brief set up +//////////////////////////////////////////////////////////////////////////////// + + setUp : function () { + internal.db._drop(cn); + collection = internal.db._create(cn); + + collection.ensureIndex({type: 'hash', name: 'hash_a', fields: ['a']}); + collection.ensureIndex({type: 'hash', name: 'hash_a_b', fields: ['a', 'b']}); + collection.ensureIndex({type: 'hash', name: 'hash_b_a', fields: ['b', 'a']}); + collection.ensureIndex({type: 'skiplist', name: 'skip_a', fields: ['a']}); + collection.ensureIndex({type: 'skiplist', name: 'skip_a_b', fields: ['a', 'b']}); + collection.ensureIndex({type: 'skiplist', name: 'skip_b_a', fields: ['b', 'a']}); + + const isMMFiles = db._engine().name === "mmfiles"; + defaultEqualityIndex = isMMFiles ? 'skip_a' : 'hash_a'; + alternateEqualityIndex = isMMFiles ? 'hash_a' : 'skip_a'; + defaultSortingIndex = isMMFiles ? 'skip_a' : 'hash_a'; + alternateSortingIndex = 'skip_a_b'; + }, + + tearDown : function () { + internal.db._drop(cn); + }, + + testFilterNoHint : function () { + const query = ` + FOR doc IN ${cn} OPTIONS {} + FILTER doc.a == 1 + RETURN doc + `; + const usedIndexes = getIndexNames(query); + assertEqual(usedIndexes.length, 1); + assertEqual(usedIndexes[0].length, 1); + assertEqual(usedIndexes[0][0], defaultEqualityIndex); + }, + + testFilterDefaultHint : function () { + const query = ` + FOR doc IN ${cn} OPTIONS {indexHint: '${defaultEqualityIndex}'} + FILTER doc.a == 1 + RETURN doc + `; + const usedIndexes = getIndexNames(query); + assertEqual(usedIndexes.length, 1); + assertEqual(usedIndexes[0].length, 1); + assertEqual(usedIndexes[0][0], defaultEqualityIndex); + }, + + testFilterDefaultHintForced : function () { + const query = ` + FOR doc IN ${cn} OPTIONS {indexHint: '${defaultEqualityIndex}', forceIndexHint: true} + FILTER doc.a == 1 + RETURN doc + `; + const usedIndexes = getIndexNames(query); + assertEqual(usedIndexes.length, 1); + assertEqual(usedIndexes[0].length, 1); + assertEqual(usedIndexes[0][0], defaultEqualityIndex); + }, + + testFilterNonexistentIndexHint : function () { + const query = ` + FOR doc IN ${cn} OPTIONS {indexHint: 'foo'} + FILTER doc.a == 1 + RETURN doc + `; + const usedIndexes = getIndexNames(query); + assertEqual(usedIndexes.length, 1); + assertEqual(usedIndexes[0].length, 1); + assertEqual(usedIndexes[0][0], defaultEqualityIndex); + }, + + testFilterNonexistentIndexHintForced : function () { + const query = ` + FOR doc IN ${cn} OPTIONS {indexHint: 'foo', forceIndexHint: true} + FILTER doc.a == 1 + RETURN doc + `; + try { + const usedIndexes = getIndexNames(query); + fail(); + } catch (err) { + assertEqual(errors.ERROR_QUERY_FORCED_INDEX_HINT_UNUSABLE.code, err.errorNum); + } + }, + + testFilterUnusableHint : function () { + const query = ` + FOR doc IN ${cn} OPTIONS {indexHint: 'hash_b_a'} + FILTER doc.a == 1 + RETURN doc + `; + const usedIndexes = getIndexNames(query); + assertEqual(usedIndexes.length, 1); + assertEqual(usedIndexes[0].length, 1); + assertEqual(usedIndexes[0][0], defaultEqualityIndex); + }, + + testFilterUnusableHintForced : function () { + const query = ` + FOR doc IN ${cn} OPTIONS {indexHint: 'hash_b_a', forceIndexHint: true} + FILTER doc.a == 1 + RETURN doc + `; + try { + const usedIndexes = getIndexNames(query); + fail(); + } catch (err) { + assertEqual(errors.ERROR_QUERY_FORCED_INDEX_HINT_UNUSABLE.code, err.errorNum); + } + }, + + testFilterTypeHint : function () { + const query = ` + FOR doc IN ${cn} OPTIONS {indexHint: '${alternateEqualityIndex}'} + FILTER doc.a == 1 + RETURN doc + `; + const usedIndexes = getIndexNames(query); + assertEqual(usedIndexes.length, 1); + assertEqual(usedIndexes[0].length, 1); + assertEqual(usedIndexes[0][0], alternateEqualityIndex); + }, + + testFilterPartialCoverageHint : function () { + const query = ` + FOR doc IN ${cn} OPTIONS {indexHint: 'skip_a_b'} + FILTER doc.a == 1 + RETURN doc + `; + const usedIndexes = getIndexNames(query); + assertEqual(usedIndexes.length, 1); + assertEqual(usedIndexes[0].length, 1); + assertEqual(usedIndexes[0][0], 'skip_a_b'); + }, + + testFilterListFirstHint : function () { + const query = ` + FOR doc IN ${cn} OPTIONS {indexHint: ['skip_a_b', '${alternateEqualityIndex}']} + FILTER doc.a == 1 + RETURN doc + `; + const usedIndexes = getIndexNames(query); + assertEqual(usedIndexes.length, 1); + assertEqual(usedIndexes[0].length, 1); + assertEqual(usedIndexes[0][0], `skip_a_b`); + }, + + testFilterListLastHint : function () { + const query = ` + FOR doc IN ${cn} OPTIONS {indexHint: ['skip_b_a', '${alternateEqualityIndex}']} + FILTER doc.a == 1 + RETURN doc + `; + const usedIndexes = getIndexNames(query); + assertEqual(usedIndexes.length, 1); + assertEqual(usedIndexes[0].length, 1); + assertEqual(usedIndexes[0][0], alternateEqualityIndex); + }, + + testFilterNestedMatchedHint : function () { + const query = ` + FOR doc IN ${cn} OPTIONS {indexHint: '${alternateEqualityIndex}'} + FILTER doc.a == 1 + FOR sub IN ${cn} OPTIONS {indexHint: '${alternateEqualityIndex}'} + FILTER sub.a == 2 + RETURN [doc, sub] + `; + const usedIndexes = getIndexNames(query); + assertEqual(usedIndexes.length, 2); + assertEqual(usedIndexes[0].length, 1); + assertEqual(usedIndexes[0][0], alternateEqualityIndex); + assertEqual(usedIndexes[1].length, 1); + assertEqual(usedIndexes[1][0], alternateEqualityIndex); + }, + + testFilterNestedUnmatchedHint : function () { + const query = ` + FOR doc IN ${cn} OPTIONS {indexHint: '${alternateEqualityIndex}'} + FILTER doc.a == 1 + FOR sub IN ${cn} OPTIONS {indexHint: 'skip_a_b'} + FILTER sub.a == 2 + RETURN [doc, sub] + `; + const usedIndexes = getIndexNames(query); + assertEqual(usedIndexes.length, 2); + assertEqual(usedIndexes[0].length, 1); + assertEqual(usedIndexes[0][0], alternateEqualityIndex); + assertEqual(usedIndexes[1].length, 1); + assertEqual(usedIndexes[1][0], 'skip_a_b'); + }, + + testSortNoHint : function () { + const query = ` + FOR doc IN ${cn} OPTIONS {} + SORT doc.a + RETURN doc + `; + const usedIndexes = getIndexNames(query); + assertEqual(usedIndexes.length, 1); + assertEqual(usedIndexes[0].length, 1); + assertEqual(usedIndexes[0][0], defaultSortingIndex); + }, + + testSortDefaultHint : function () { + const query = ` + FOR doc IN ${cn} OPTIONS {indexHint: '${defaultSortingIndex}'} + SORT doc.a + RETURN doc + `; + const usedIndexes = getIndexNames(query); + assertEqual(usedIndexes.length, 1); + assertEqual(usedIndexes[0].length, 1); + assertEqual(usedIndexes[0][0], defaultSortingIndex); + }, + + testSortDefaultHintForced : function () { + const query = ` + FOR doc IN ${cn} OPTIONS {indexHint: '${defaultSortingIndex}', forceIndexHint: true} + SORT doc.a + RETURN doc + `; + const usedIndexes = getIndexNames(query); + assertEqual(usedIndexes.length, 1); + assertEqual(usedIndexes[0].length, 1); + assertEqual(usedIndexes[0][0], defaultSortingIndex); + }, + + testSortNonexistentIndexHint : function () { + const query = ` + FOR doc IN ${cn} OPTIONS {indexHint: 'foo'} + SORT doc.a + RETURN doc + `; + const usedIndexes = getIndexNames(query); + assertEqual(usedIndexes.length, 1); + assertEqual(usedIndexes[0].length, 1); + assertEqual(usedIndexes[0][0], defaultSortingIndex); + }, + + testSortNonexistentIndexHintForced : function () { + const query = ` + FOR doc IN ${cn} OPTIONS {indexHint: 'foo', forceIndexHint: true} + SORT doc.a + RETURN doc + `; + try { + const usedIndexes = getIndexNames(query); + fail(); + } catch (err) { + assertEqual(errors.ERROR_QUERY_FORCED_INDEX_HINT_UNUSABLE.code, err.errorNum); + } + }, + + testSortUnusableHint : function () { + const query = ` + FOR doc IN ${cn} OPTIONS {indexHint: 'skip_b_a'} + SORT doc.a + RETURN doc + `; + const usedIndexes = getIndexNames(query); + assertEqual(usedIndexes.length, 1); + assertEqual(usedIndexes[0].length, 1); + assertEqual(usedIndexes[0][0], defaultSortingIndex); + }, + + testSortUnusableHintForced : function () { + const query = ` + FOR doc IN ${cn} OPTIONS {indexHint: 'skip_b_a', forceIndexHint: true} + SORT doc.a + RETURN doc + `; + try { + const usedIndexes = getIndexNames(query); + fail(); + } catch (err) { + assertEqual(errors.ERROR_QUERY_FORCED_INDEX_HINT_UNUSABLE.code, err.errorNum); + } + }, + + testSortPartialCoverageHint : function () { + const query = ` + FOR doc IN ${cn} OPTIONS {indexHint: '${alternateSortingIndex}'} + SORT doc.a + RETURN doc + `; + const usedIndexes = getIndexNames(query); + assertEqual(usedIndexes.length, 1); + assertEqual(usedIndexes[0].length, 1); + assertEqual(usedIndexes[0][0], alternateSortingIndex); + }, + + testSortListFirstHint : function () { + const query = ` + FOR doc IN ${cn} OPTIONS {indexHint: ['${alternateSortingIndex}', '${defaultSortingIndex}']} + SORT doc.a + RETURN doc + `; + const usedIndexes = getIndexNames(query); + assertEqual(usedIndexes.length, 1); + assertEqual(usedIndexes[0].length, 1); + assertEqual(usedIndexes[0][0], alternateSortingIndex); + }, + + testSortListLastHint : function () { + const query = ` + FOR doc IN ${cn} OPTIONS {indexHint: ['skip_b_a', '${alternateSortingIndex}']} + SORT doc.a + RETURN doc + `; + const usedIndexes = getIndexNames(query); + assertEqual(usedIndexes.length, 1); + assertEqual(usedIndexes[0].length, 1); + assertEqual(usedIndexes[0][0], alternateSortingIndex); + }, + + }; +} + +//////////////////////////////////////////////////////////////////////////////// +/// @brief executes the test suite +//////////////////////////////////////////////////////////////////////////////// + +jsunity.run(ahuacatlSkiplistOverlappingTestSuite); + +return jsunity.done();