diff --git a/arangod/Aql/AstNode.cpp b/arangod/Aql/AstNode.cpp index 7284062ad4ea..e5ab01d74d35 100644 --- a/arangod/Aql/AstNode.cpp +++ b/arangod/Aql/AstNode.cpp @@ -31,6 +31,7 @@ #include "Aql/Query.h" #include "Aql/Scopes.h" #include "Aql/types.h" +#include "Assertions/Assert.h" #include "Basics/FloatingPoint.h" #include "Basics/StringUtils.h" #include "Basics/Utf8Helper.h" @@ -1401,6 +1402,17 @@ bool AstNode::isTrue() const { // ! false => true return true; } + } else if (type == NODE_TYPE_OPERATOR_BINARY_IN || + type == NODE_TYPE_OPERATOR_BINARY_NIN) { + // Handle empty IN arrays: x.name NOT IN [] → true + if (numMembers() >= 2) { + TRI_ASSERT(numMembers() == 2); + AstNode const* rhs = getMember(1); + if (rhs != nullptr && rhs->type == NODE_TYPE_ARRAY && + rhs->numMembers() == 0) { + return (type == NODE_TYPE_OPERATOR_BINARY_NIN); + } + } } return false; @@ -1445,6 +1457,17 @@ bool AstNode::isFalse() const { // ! true => false return true; } + } else if (type == NODE_TYPE_OPERATOR_BINARY_IN || + type == NODE_TYPE_OPERATOR_BINARY_NIN) { + // Handle empty IN arrays: x.name IN [] → false + if (numMembers() >= 2) { + TRI_ASSERT(numMembers() == 2); + AstNode const* rhs = getMember(1); + if (rhs != nullptr && rhs->type == NODE_TYPE_ARRAY && + rhs->numMembers() == 0) { + return (type == NODE_TYPE_OPERATOR_BINARY_IN); + } + } } return false; diff --git a/arangod/Aql/Condition.cpp b/arangod/Aql/Condition.cpp index 910fe145d5b7..7a6a5b15ca57 100644 --- a/arangod/Aql/Condition.cpp +++ b/arangod/Aql/Condition.cpp @@ -1467,18 +1467,128 @@ void Condition::optimize(ExecutionPlan* plan, bool multivalued) { if (op->type == NODE_TYPE_OPERATOR_BINARY_IN) { ++inComparisons; auto deduplicated = deduplicateInOperation(op); + + // x IN [a] → x == a + if (deduplicated->numMembers() == 2) { + auto rhs = deduplicated->getMemberUnchecked(1); + if (rhs->type == NODE_TYPE_ARRAY && rhs->isConstant() && + rhs->numMembers() == 1) { + auto lhs = deduplicated->getMemberUnchecked(0); + auto optimized = plan->getAst()->createNodeBinaryOperator( + NODE_TYPE_OPERATOR_BINARY_EQ, lhs, rhs->getMemberUnchecked(0)); + andNode->changeMember(j, optimized); + --inComparisons; + continue; + } + } + andNode->changeMember(j, deduplicated); } } andNumMembers = andNode->numMembers(); - if (andNumMembers <= 1) { - // simple AND item with 0 or 1 members. nothing to do - ++r; + // Remove AND branch if any condition is false + bool andIsFalse = false; + for (size_t j = 0; j < andNumMembers; ++j) { + auto op = andNode->getMemberUnchecked(j); + if (op->isFalse()) { + andIsFalse = true; + break; + } + } + + if (andIsFalse) { + _root->removeMemberUncheckedUnordered(r); + retry = true; + n = _root->numMembers(); + continue; + } + + // Remove redundant true conditions + if (andNumMembers > 1) { + for (size_t j = andNumMembers; j > 0; --j) { + auto op = andNode->getMemberUnchecked(j - 1); + + if (op->isTrue()) { + andNode->removeMemberUncheckedUnordered(j - 1); + --andNumMembers; + } + } + } + + // Remove duplicate conditions + for (size_t j = andNumMembers; j > 1; --j) { + auto op1 = andNode->getMemberUnchecked(j - 1); + + if (!op1->isDeterministic()) { + continue; + } + + for (size_t k = j - 1; k > 0; --k) { + auto op2 = andNode->getMemberUnchecked(k - 1); + + if (!op2->isDeterministic()) { + continue; + } + + if (op1->type == op2->type && op1->numMembers() == op2->numMembers()) { + bool isDuplicate = false; + + if (op1->type == NODE_TYPE_OPERATOR_BINARY_IN && + op1->numMembers() == 2 && op2->numMembers() == 2) { + auto lhs1 = op1->getMember(0); + auto rhs1 = op1->getMember(1); + auto lhs2 = op2->getMember(0); + auto rhs2 = op2->getMember(1); + + if (compareAstNodes(lhs1, lhs2, false) == 0 && + rhs1->type == NODE_TYPE_ARRAY && + rhs2->type == NODE_TYPE_ARRAY && + rhs1->numMembers() == rhs2->numMembers()) { + isDuplicate = true; + for (size_t m = 0; m < rhs1->numMembers(); ++m) { + if (compareAstNodes(rhs1->getMember(m), rhs2->getMember(m), + true) != 0) { + isDuplicate = false; + break; + } + } + } + } else if (op1->isComparisonOperator() && op1->numMembers() == 2) { + auto lhs1 = op1->getMember(0); + auto rhs1 = op1->getMember(1); + auto lhs2 = op2->getMember(0); + auto rhs2 = op2->getMember(1); + + if (compareAstNodes(lhs1, lhs2, false) == 0 && + compareAstNodes(rhs1, rhs2, true) == 0) { + isDuplicate = true; + } + } + + if (isDuplicate) { + andNode->removeMemberUncheckedUnordered(j - 1); + --andNumMembers; + break; + } + } + } + } + + if (andNumMembers == 0) { + _root->removeMemberUncheckedUnordered(r); + retry = true; n = _root->numMembers(); continue; } + // Keep DNF structure: OR(AND(x)) stays as-is + // Unwrapping would break the invariant that all OR children are AND nodes + if (andNumMembers == 1) { + ++r; + continue; + } + TRI_ASSERT(andNumMembers > 1); // sort AND parts of each sub-condition so > and >= come before < and <= @@ -1628,10 +1738,21 @@ void Condition::optimize(ExecutionPlan* plan, bool multivalued) { // merge IN with IN on same attribute TRI_ASSERT(rightNode->numMembers() == 2); + auto mergedArray = mergeInOperations(leftNode, rightNode); auto merged = _ast->createNodeBinaryOperator( NODE_TYPE_OPERATOR_BINARY_IN, - leftNode->getMemberUnchecked(0), - mergeInOperations(leftNode, rightNode)); + leftNode->getMemberUnchecked(0), mergedArray); + + // Optimize IN with single value to equality: x IN [5] → x == 5 + if (mergedArray->type == NODE_TYPE_ARRAY && + mergedArray->isConstant() && + mergedArray->numMembers() == 1) { + auto lhs = leftNode->getMemberUnchecked(0); + merged = plan->getAst()->createNodeBinaryOperator( + NODE_TYPE_OPERATOR_BINARY_EQ, lhs, + mergedArray->getMemberUnchecked(0)); + } + andNode->removeMemberUncheckedUnordered(rightPos); andNode->changeMember(leftPos, merged); goto restartThisOrItem; @@ -1670,6 +1791,15 @@ void Condition::optimize(ExecutionPlan* plan, bool multivalued) { // use the new array of values leftNode->changeMember(1, inNode); + // Optimize IN with single value to equality: x IN [5] → x == 5 + if (inNode->numMembers() == 1) { + auto lhs = leftNode->getMemberUnchecked(0); + auto optimized = plan->getAst()->createNodeBinaryOperator( + NODE_TYPE_OPERATOR_BINARY_EQ, lhs, + inNode->getMemberUnchecked(0)); + andNode->changeMember(leftPos, optimized); + } + // remove the other operator andNode->removeMemberUncheckedUnordered(rightPos); goto restartThisOrItem; @@ -1756,6 +1886,95 @@ void Condition::optimize(ExecutionPlan* plan, bool multivalued) { // now recalculate the number and don't modify r! n = _root->numMembers(); } + + if (_root->numMembers() == 0) { + return; + } + + // Remove duplicate OR branches + n = _root->numMembers(); + for (size_t i = n; i > 1; --i) { + auto branch1 = _root->getMemberUnchecked(i - 1); + if (branch1->type != NODE_TYPE_OPERATOR_NARY_AND) { + continue; + } + + for (size_t j = i - 1; j > 0; --j) { + auto branch2 = _root->getMemberUnchecked(j - 1); + if (branch2->type != NODE_TYPE_OPERATOR_NARY_AND) { + continue; + } + + if (branch1->numMembers() != branch2->numMembers()) { + continue; + } + + bool isDuplicate = true; + for (size_t k = 0; k < branch1->numMembers(); ++k) { + auto cond1 = branch1->getMemberUnchecked(k); + auto cond2 = branch2->getMemberUnchecked(k); + + if (cond1->type != cond2->type || + cond1->numMembers() != cond2->numMembers()) { + isDuplicate = false; + break; + } + + if (cond1->isDeterministic() && cond2->isDeterministic()) { + if (cond1->type == NODE_TYPE_OPERATOR_BINARY_IN && + cond1->numMembers() == 2 && cond2->numMembers() == 2) { + auto lhs1 = cond1->getMember(0); + auto rhs1 = cond1->getMember(1); + auto lhs2 = cond2->getMember(0); + auto rhs2 = cond2->getMember(1); + + if (compareAstNodes(lhs1, lhs2, false) != 0 || + rhs1->type != NODE_TYPE_ARRAY || + rhs2->type != NODE_TYPE_ARRAY || + rhs1->numMembers() != rhs2->numMembers()) { + isDuplicate = false; + break; + } + + for (size_t m = 0; m < rhs1->numMembers(); ++m) { + if (compareAstNodes(rhs1->getMember(m), rhs2->getMember(m), + true) != 0) { + isDuplicate = false; + break; + } + } + if (!isDuplicate) break; + } else if (cond1->isComparisonOperator() && + cond1->numMembers() == 2) { + auto lhs1 = cond1->getMember(0); + auto rhs1 = cond1->getMember(1); + auto lhs2 = cond2->getMember(0); + auto rhs2 = cond2->getMember(1); + + if (compareAstNodes(lhs1, lhs2, false) != 0 || + compareAstNodes(rhs1, rhs2, true) != 0) { + isDuplicate = false; + break; + } + } else { + if (cond1->toString() != cond2->toString()) { + isDuplicate = false; + break; + } + } + } else { + isDuplicate = false; + break; + } + } + + if (isDuplicate) { + _root->removeMemberUncheckedUnordered(i - 1); + --n; + break; // Found duplicate, move to next branch + } + } + } } /// @brief registers an attribute access for a particular (collection) variable diff --git a/arangod/Aql/OptimizerRule.h b/arangod/Aql/OptimizerRule.h index 92b7ab2d703d..a625b6aa9e8a 100644 --- a/arangod/Aql/OptimizerRule.h +++ b/arangod/Aql/OptimizerRule.h @@ -193,6 +193,9 @@ struct OptimizerRule { // replace simple OR conditions with IN replaceOrWithInRule, + // replace ANY == conditions with IN + replaceAnyEqWithInRule, + // remove redundant OR conditions removeRedundantOrRule, diff --git a/arangod/Aql/OptimizerRules.cpp b/arangod/Aql/OptimizerRules.cpp index 2749c824fc8c..14a4a1f65b0d 100644 --- a/arangod/Aql/OptimizerRules.cpp +++ b/arangod/Aql/OptimizerRules.cpp @@ -45,6 +45,7 @@ #include "Aql/ExecutionNode/EnumeratePathsNode.h" #include "Aql/ExecutionNode/ExecutionNode.h" #include "Aql/ExecutionNode/FilterNode.h" +#include "Aql/ExecutionNode/NoResultsNode.h" #include "Aql/ExecutionNode/GatherNode.h" #include "Aql/ExecutionNode/IResearchViewNode.h" #include "Aql/ExecutionNode/IndexNode.h" @@ -60,9 +61,9 @@ #include "Aql/ExecutionNode/ScatterNode.h" #include "Aql/ExecutionNode/ShortestPathNode.h" #include "Aql/ExecutionNode/SortNode.h" -#include "Aql/ExecutionNode/SubqueryEndExecutionNode.h" #include "Aql/ExecutionNode/SubqueryNode.h" #include "Aql/ExecutionNode/SubqueryStartExecutionNode.h" +#include "Aql/ExecutionNode/SubqueryEndExecutionNode.h" #include "Aql/ExecutionNode/TraversalNode.h" #include "Aql/ExecutionNode/MaterializeNode.h" #include "Aql/ExecutionNode/UpdateNode.h" @@ -72,10 +73,10 @@ #include "Aql/Expression.h" #include "Aql/Function.h" #include "Aql/IndexHint.h" -#include "Aql/IndexStreamIterator.h" #include "Aql/Optimizer.h" #include "Aql/OptimizerUtils.h" #include "Aql/Projections.h" +#include "Aql/Quantifier.h" #include "Aql/Query.h" #include "Aql/SortCondition.h" #include "Aql/SortElement.h" @@ -1519,6 +1520,14 @@ void arangodb::aql::removeUnnecessaryFiltersRule( // remove filter node and merge with following node toUnlink.emplace(n); modified = true; + } else if (root->isFalse() && + rule.level == OptimizerRule::removeUnnecessaryFiltersRule2) { + // filter is always false - replace with NoResultsNode + // Only do this in the second pass (level 210) after all transformations + // This allows isFalse() to catch IN [] expressions created by rules + auto noRes = plan->createNode(plan.get(), plan->nextId()); + plan->replaceNode(n, noRes); + modified = true; } // before 3.6, if the filter is always false (i.e. root->isFalse()), at this // point a NoResultsNode was inserted. @@ -5796,22 +5805,34 @@ struct CommonNodeFinder { } }; -/// @brief auxilliary struct for the OR-to-IN conversion -struct OrSimplifier { - Ast* ast; - ExecutionPlan* plan; - - OrSimplifier(Ast* ast, ExecutionPlan* plan) : ast(ast), plan(plan) {} - - std::string stringifyNode(AstNode const* node) const { +/// @brief common utilities for checking and stringifying AST nodes +struct SimplifierHelper { + /// @brief convert AST node to its string representation + /// @param node The node to stringify (may be nullptr) + /// @return The string representation, or empty string on failure or nullptr + static std::string stringifyNode(AstNode const* node) { + if (node == nullptr) { + return std::string(); + } try { return node->toString(); } catch (...) { + return std::string(); } - return std::string(); } - bool qualifies(AstNode const* node, std::string& attributeName) const { + /// @brief Check if a node qualifies as an attribute/reference expression + /// + /// Constants are excluded as they cannot be used for index lookups. + /// + /// @param node The node to check (may be nullptr) + /// @param attributeName Output parameter: the stringified attribute name + /// @return true if the node is a valid attribute/reference expression + static bool qualifies(AstNode const* node, std::string& attributeName) { + if (node == nullptr) { + return false; + } + if (node->isConstant()) { return false; } @@ -5825,6 +5846,22 @@ struct OrSimplifier { return false; } +}; + +/// @brief auxilliary struct for the OR-to-IN conversion +struct OrSimplifier { + Ast* ast; + ExecutionPlan* plan; + + OrSimplifier(Ast* ast, ExecutionPlan* plan) : ast(ast), plan(plan) {} + + std::string stringifyNode(AstNode const* node) const { + return SimplifierHelper::stringifyNode(node); + } + + bool qualifies(AstNode const* node, std::string& attributeName) const { + return SimplifierHelper::qualifies(node, attributeName); + } bool detect(AstNode const* node, bool preferRight, std::string& attributeName, AstNode const*& attr, AstNode const*& value) const { @@ -6034,6 +6071,224 @@ void arangodb::aql::replaceOrWithInRule(Optimizer* opt, opt->addPlan(std::move(plan), rule, modified); } +/// @brief for the ANY-to-IN conversion +/// +/// Transforms expressions of the form: +/// ['Alice','Bob', 'Carol'] ANY == p.name +/// into: +/// p.name IN ['Alice', 'Bob', 'Carol'] +/// +/// enables the optimizer to use index lookups and other optimizations +/// that are not yet available for ANY == expressions. +struct AnySimplifier { + Ast& ast; + ExecutionPlan& plan; + + explicit AnySimplifier(Ast& ast, ExecutionPlan& plan) + : ast(ast), plan(plan) {} + + /// @brief Safely convert an AST node to its string representation + /// @param node The node to stringify + /// @return The string representation, or empty string on failure + std::string stringifyNode(AstNode const* node) const { + return SimplifierHelper::stringifyNode(node); + } + + /// @brief Check if a node qualifies as an attribute/reference expression + /// @param node The node to check + /// @param attributeName Output parameter: the stringified attribute name + /// @return true if the node is a valid attribute/reference expression + bool qualifies(AstNode const* node, std::string& attributeName) const { + return SimplifierHelper::qualifies(node, attributeName); + } + + /// @brief Extract attribute and array from an ANY == expression + /// @param node The ANY == node to analyze (must have 3 members: lhs, rhs, + /// quantifier) + /// @return Optional pair containing (attribute node, array node) if both + /// found unambiguously + std::optional> extractAttributeAndArray( + AstNode const* node) const { + TRI_ASSERT(node->numMembers() == 3); + + // Member 2 is always the quantifier - must be ANY for this transformation + auto quantifier = node->getMember(2); + if (quantifier == nullptr || !Quantifier::isAny(quantifier)) { + return std::nullopt; + } + + // Members 0 and 1 are the left and right expressions + // We need to find which one is the array and which one is the attribute + auto lhs = node->getMember(0); + auto rhs = node->getMember(1); + + if (lhs == nullptr || rhs == nullptr) { + return std::nullopt; + } + + AstNode* attr = nullptr; + AstNode* array = nullptr; + std::string tmpName; // Unused, but required by qualifies() + + // Check if lhs is the array and rhs is the attribute + if (lhs->isArray() && lhs->isDeterministic() && qualifies(rhs, tmpName)) { + array = lhs; + attr = rhs; + } + // Check if rhs is the array and lhs is the attribute + else if (rhs->isArray() && rhs->isDeterministic() && + qualifies(lhs, tmpName)) { + array = rhs; + attr = lhs; + } else { + // Neither combination works - cannot transform + return std::nullopt; + } + + return std::make_pair(attr, array); + } + + /// @brief Rewrite an ANY == expression to an IN expression + /// + /// Transforms: ['Alice','Bob', 'Carol'] ANY == p.name + /// into: p.name IN ['Alice', 'Bob', 'Carol'] + /// + /// @param node The ANY == node to transform (must be + /// NODE_TYPE_OPERATOR_BINARY_ARRAY_EQ) + AstNode* simplifyAnyEq(AstNode const* node) const { + if (node == nullptr) { + return nullptr; + } + + if (node->type != NODE_TYPE_OPERATOR_BINARY_ARRAY_EQ) { + return const_cast(node); + } + + auto result = extractAttributeAndArray(node); + if (!result.has_value()) { + // Cannot transform - return original node + return const_cast(node); + } + + auto [attr, array] = result.value(); + + // Build: attr IN array + return ast.createNodeBinaryOperator(NODE_TYPE_OPERATOR_BINARY_IN, attr, + array); + } + + /// @brief Recursively simplify the expression tree (post-order traversal) + /// + /// Processes children first, then attempts to transform the current node + /// if it is an ANY == expression. + /// + /// @param node The root of the expression tree to simplify + /// @return The simplified expression tree + AstNode* simplify(AstNode const* node) const { + if (node == nullptr) { + return nullptr; + } + + // First, recurse into children (post-order traversal) + size_t const numMembers = node->numMembers(); + bool childrenModified = false; + containers::SmallVector newChildren; + newChildren.reserve(numMembers); + + for (size_t i = 0; i < numMembers; ++i) { + AstNode* child = node->getMember(i); + AstNode* newChild = simplify(child); + newChildren.push_back(newChild); + if (newChild != child) { + childrenModified = true; + } + } + + // If children were modified, create a new node with the modified children + AstNode* result = const_cast(node); + if (childrenModified) { + if (node->type == NODE_TYPE_OPERATOR_BINARY_AND) { + result = ast.createNodeBinaryOperator(node->type, newChildren[0], + newChildren[1]); + } else if (node->type == NODE_TYPE_OPERATOR_NARY_AND) { + result = ast.createNodeNaryOperator(node->type); + for (auto* child : newChildren) { + result->addMember(child); + } + } else if (numMembers == 2 && + (node->type == NODE_TYPE_OPERATOR_BINARY_OR || + node->type == NODE_TYPE_OPERATOR_BINARY_EQ || + node->type == NODE_TYPE_OPERATOR_BINARY_NE || + node->type == NODE_TYPE_OPERATOR_BINARY_LT || + node->type == NODE_TYPE_OPERATOR_BINARY_LE || + node->type == NODE_TYPE_OPERATOR_BINARY_GT || + node->type == NODE_TYPE_OPERATOR_BINARY_GE)) { + result = ast.createNodeBinaryOperator(node->type, newChildren[0], + newChildren[1]); + } else { + // For other node types, clone and replace children + result = ast.shallowCopyForModify(node); + for (size_t i = 0; i < numMembers; ++i) { + result->changeMember(i, newChildren[i]); + } + } + } + + // Now try to rewrite this node itself if it is ANY == + if (result->type == NODE_TYPE_OPERATOR_BINARY_ARRAY_EQ) { + return simplifyAnyEq(result); + } + + return result; + } +}; + +void arangodb::aql::replaceAnyEqWithInRule(Optimizer* opt, + std::unique_ptr plan, + OptimizerRule const& rule) { + containers::SmallVector nodes; + plan->findNodesOfType(nodes, EN::FILTER, true); + + bool modified = false; + for (auto const& n : nodes) { + TRI_ASSERT(n->hasDependency()); + auto const dep = n->getFirstDependency(); + + if (dep->getType() != EN::CALCULATION) { + continue; + } + + auto fn = ExecutionNode::castTo(n); + auto cn = ExecutionNode::castTo(dep); + auto outVar = cn->outVariable(); + + if (outVar != fn->inVariable()) { + continue; + } + + auto root = cn->expression()->node(); + + AnySimplifier simplifier(*plan->getAst(), *plan.get()); + auto newRoot = simplifier.simplify(root); + + if (newRoot != root) { + auto expr = std::make_unique(plan->getAst(), newRoot); + + TRI_IF_FAILURE("OptimizerRules::replaceAnyEqWithInRuleOom") { + THROW_ARANGO_EXCEPTION(TRI_ERROR_DEBUG); + } + + ExecutionNode* newNode = plan->createNode( + plan.get(), plan->nextId(), std::move(expr), outVar); + + plan->replaceNode(cn, newNode); + modified = true; + } + } + + opt->addPlan(std::move(plan), rule, modified); +} + struct RemoveRedundantOr { AstNode const* bestValue = nullptr; AstNodeType comparison; diff --git a/arangod/Aql/OptimizerRules.h b/arangod/Aql/OptimizerRules.h index 4f5ccadac9c5..195bd34d0489 100644 --- a/arangod/Aql/OptimizerRules.h +++ b/arangod/Aql/OptimizerRules.h @@ -270,6 +270,13 @@ void undistributeRemoveAfterEnumCollRule(Optimizer*, void replaceOrWithInRule(Optimizer*, std::unique_ptr, OptimizerRule const&); +/// @brief Rewrite: +/// ['Alice','Bob', 'Carol'] ANY == p.name +/// into: +/// p.name IN ['Alice', 'Bob', 'Carol'] +void replaceAnyEqWithInRule(Optimizer* opt, std::unique_ptr plan, + OptimizerRule const& rule); + void removeRedundantOrRule(Optimizer*, std::unique_ptr, OptimizerRule const&); diff --git a/arangod/Aql/OptimizerRulesFeature.cpp b/arangod/Aql/OptimizerRulesFeature.cpp index fc9b5d2e5cce..769ddf0700e7 100644 --- a/arangod/Aql/OptimizerRulesFeature.cpp +++ b/arangod/Aql/OptimizerRulesFeature.cpp @@ -353,6 +353,14 @@ are not used in data modification queries.)"); R"(Combine multiple `OR` equality conditions on the same variable or attribute with an `IN` condition.)"); + // try to replace ANY == array comparisons with IN + registerRule( + "replace-any-eq-with-in", replaceAnyEqWithInRule, + OptimizerRule::replaceAnyEqWithInRule, + OptimizerRule::makeFlags(OptimizerRule::Flags::CanBeDisabled), + R"(Replace `ANY ==` array comparison expressions with equivalent `IN` +expressions to enable further optimizations and index usage.)"); + // try to remove redundant OR conditions registerRule("remove-redundant-or", removeRedundantOrRule, OptimizerRule::removeRedundantOrRule, diff --git a/tests/js/client/aql/aql-optimizer-rule-replace-any-with-in.js b/tests/js/client/aql/aql-optimizer-rule-replace-any-with-in.js new file mode 100644 index 000000000000..b99474d56139 --- /dev/null +++ b/tests/js/client/aql/aql-optimizer-rule-replace-any-with-in.js @@ -0,0 +1,812 @@ +/*jshint globalstrict:false, strict:false, maxlen: 500 */ +/*global assertEqual, assertTrue, fail */ + +// ////////////////////////////////////////////////////////////////////////////// +// / DISCLAIMER +// / +// / Copyright 2014-2025 Arango GmbH, Cologne, Germany +// / Copyright 2004-2014 triAGENS GmbH, Cologne, Germany +// / +// / Licensed under the Business Source License 1.1 (the "License"); +// / you may not use this file except in compliance with the License. +// / You may obtain a copy of the License at +// / +// / https://github.com/arangodb/arangodb/blob/devel/LICENSE +// / +// / 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 Julia Puget +/// @author Copyright 2014, triAGENS GmbH, Cologne, Germany +// ////////////////////////////////////////////////////////////////////////////// + +var internal = require("internal"); +var jsunity = require("jsunity"); +var helper = require("@arangodb/aql-helper"); +var getQueryResults = helper.getQueryResults; +var findExecutionNodes = helper.findExecutionNodes; +const db = require('internal').db; +let {instanceRole} = require('@arangodb/testutils/instance'); +let IM = global.instanceManager; + +function NewAqlReplaceAnyWithINTestSuite() { + var replace; + var ruleName = "replace-any-eq-with-in"; + + var getPlan = function (query, params, options) { + return db._createStatement({query: query, bindVars: params, options: options}).explain().plan; + }; + + var ruleIsNotUsed = function (query, params) { + var plan = getPlan(query, params, {optimizer: {rules: ["-all", "+" + ruleName]}}); + assertTrue(plan.rules.indexOf(ruleName) === -1, "Rule should not be used: " + query); + }; + + var executeWithRule = function (query, params) { + return db._query(query, params, {optimizer: {rules: ["-all", "+" + ruleName]}}).toArray(); + }; + + var executeWithoutRule = function (query, params) { + return db._query(query, params, {optimizer: {rules: ["-all"]}}).toArray(); + }; + + var executeWithOrRule = function (query, params) { + return db._query(query, params, {optimizer: {rules: ["-all", "+replace-or-with-in"]}}).toArray(); + }; + + var verifyExecutionPlan = function (query, params) { + var explainWithRule = db._createStatement({ + query: query, + bindVars: params || {}, + options: {optimizer: {rules: ["-all", "+" + ruleName]}} + }).explain(); + + var explainWithoutRule = db._createStatement({ + query: query, + bindVars: params || {}, + options: {optimizer: {rules: ["-all"]}} + }).explain(); + + var planWithRule = explainWithRule.plan; + var planWithoutRule = explainWithoutRule.plan; + + assertTrue(planWithRule.rules.indexOf(ruleName) !== -1, + "Plan with rule enabled should contain rule '" + ruleName + "': " + query); + assertTrue(planWithoutRule.rules.indexOf(ruleName) === -1, + "Plan without rule should NOT contain rule '" + ruleName + "': " + query); + + var filterNodesWith = findExecutionNodes(planWithRule, "FilterNode"); + var filterNodesWithout = findExecutionNodes(planWithoutRule, "FilterNode"); + var calcNodesWith = findExecutionNodes(planWithRule, "CalculationNode"); + var calcNodesWithout = findExecutionNodes(planWithoutRule, "CalculationNode"); + + assertTrue(filterNodesWith.length > 0 && filterNodesWithout.length > 0, + "Plans should have FilterNodes: " + query); + assertTrue(calcNodesWith.length > 0 && calcNodesWithout.length > 0, + "Plans should have CalculationNodes: " + query); + + assertTrue(planWithRule.nodes.length > 0, "Plan with rule should have nodes: " + query); + assertTrue(planWithoutRule.nodes.length > 0, "Plan without rule should have nodes: " + query); + + return {withRule: planWithRule, withoutRule: planWithoutRule}; + }; + + var verifyPlansDifferent = function (planWithRule, planWithoutRule, query) { + assertTrue(planWithRule.rules.indexOf(ruleName) !== -1, + "Plan with rule enabled should contain the rule: " + query); + assertTrue(planWithoutRule.rules.indexOf(ruleName) === -1, + "Plan without rule should not contain the rule: " + query); + + var calcNodesWith = findExecutionNodes(planWithRule, "CalculationNode"); + var calcNodesWithout = findExecutionNodes(planWithoutRule, "CalculationNode"); + + assertTrue(calcNodesWith.length > 0 || calcNodesWithout.length > 0, + "Plans should have calculation nodes: " + query); + + assertTrue(planWithRule.nodes.length > 0, "Plan with rule should have nodes"); + assertTrue(planWithoutRule.nodes.length > 0, "Plan without rule should have nodes"); + }; + + return { + + setUpAll: function () { + IM.debugClearFailAt(); + internal.db._drop("UnitTestsNewAqlReplaceAnyWithINTestSuite"); + replace = internal.db._create("UnitTestsNewAqlReplaceAnyWithINTestSuite"); + + let docs = []; + for (var i = 1; i <= 10; ++i) { + docs.push({"value": i, "name": "Alice", "tags": ["a", "b"], "categories": ["x", "y"]}); + docs.push({"value": i + 10, "name": "Bob", "tags": ["b", "c"], "categories": ["y", "z"]}); + docs.push({"value": i + 20, "name": "Carol", "tags": ["c", "d"], "categories": ["z"]}); + docs.push({"a": {"b": i}}); + } + replace.insert(docs); + + replace.ensureIndex({type: "persistent", fields: ["name"]}); + replace.ensureIndex({type: "persistent", fields: ["a.b"]}); + }, + + tearDownAll: function () { + IM.debugClearFailAt(); + internal.db._drop("UnitTestsNewAqlReplaceAnyWithINTestSuite"); + replace = null; + }, + + setUp: function () { + IM.debugClearFailAt(); + }, + + tearDown: function () { + IM.debugClearFailAt(); + }, + + testOom: function () { + if (!IM.debugCanUseFailAt()) { + return; + } + IM.debugSetFailAt("OptimizerRules::replaceAnyWithInRuleOom"); + try { + db._query("FOR i IN 1..10 FILTER ['Alice', 'Bob'] ANY == i RETURN i"); + fail(); + } catch (err) { + assertEqual(internal.errors.ERROR_DEBUG.code, err.errorNum); + } + }, + + testExecutionPlanVerification: function () { + var query = "FOR x IN " + replace.name() + + " FILTER ['Alice', 'Bob'] ANY == x.name SORT x.value RETURN x.value"; + + verifyExecutionPlan(query, {}); + }, + + testFiresBasic: function () { + var query = "FOR x IN " + replace.name() + + " FILTER ['Alice', 'Bob'] ANY == x.name SORT x.value RETURN x.value"; + + var plans = verifyExecutionPlan(query, {}); + verifyPlansDifferent(plans.withRule, plans.withoutRule, query); + + var expected = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]; + var actual = getQueryResults(query); + assertEqual(expected, actual); + + var withRule = executeWithRule(query, {}); + var withoutRule = executeWithoutRule(query, {}); + assertEqual(withRule, withoutRule, "Results with and without rule should match"); + + var orQuery = "FOR x IN " + replace.name() + + " FILTER x.name == 'Alice' || x.name == 'Bob' SORT x.value RETURN x.value"; + var orResult = executeWithOrRule(orQuery, {}); + assertEqual(withRule, orResult, "Results with ANY == should match OR query"); + }, + + testFiresSingleValue: function () { + var query = "FOR x IN " + replace.name() + + " FILTER ['Alice'] ANY == x.name SORT x.value RETURN x.value"; + + var plans = verifyExecutionPlan(query, {}); + verifyPlansDifferent(plans.withRule, plans.withoutRule, query); + + var expected = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + var actual = getQueryResults(query); + assertEqual(expected, actual); + + var withRule = executeWithRule(query, {}); + var withoutRule = executeWithoutRule(query, {}); + assertEqual(withRule, withoutRule, "Results with and without rule should match"); + + var orQuery = "FOR x IN " + replace.name() + + " FILTER x.name == 'Alice' SORT x.value RETURN x.value"; + var orResult = executeWithOrRule(orQuery, {}); + assertEqual(withRule, orResult, "Results with ANY == should match OR query"); + }, + + testFiresEmptyArray: function () { + var query = "FOR x IN " + replace.name() + + " FILTER [] ANY == x.name RETURN x.value"; + + var plans = verifyExecutionPlan(query, {}); + verifyPlansDifferent(plans.withRule, plans.withoutRule, query); + + var expected = []; + var actual = getQueryResults(query); + assertEqual(expected, actual); + + var withRule = executeWithRule(query, {}); + var withoutRule = executeWithoutRule(query, {}); + assertEqual(withRule, withoutRule, "Results with and without rule should match"); + + var inQuery = "FOR x IN " + replace.name() + + " FILTER x.name IN [] RETURN x.value"; + var inResult = executeWithOrRule(inQuery, {}); + assertEqual(withRule, inResult, "Results with ANY == should match IN query"); + }, + + testFiresManyValues: function () { + var query = "FOR x IN " + replace.name() + + " FILTER ['Alice', 'Bob', 'Carol', 'David', " + + " 'Eve', 'Frank', 'Grace', 'Henry'] " + + " ANY == x.name SORT x.value RETURN x.value"; + + var plans = verifyExecutionPlan(query, {}); + verifyPlansDifferent(plans.withRule, plans.withoutRule, query); + + var expected = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30]; + var actual = getQueryResults(query); + assertEqual(expected, actual); + + var withRule = executeWithRule(query, {}); + var withoutRule = executeWithoutRule(query, {}); + assertEqual(withRule, withoutRule, "Results with and without rule should match"); + + var orQuery = "FOR x IN " + replace.name() + + " FILTER x.name == 'Alice' || x.name == 'Bob' || x.name == 'Carol' || x.name == 'David' ||" + + " x.name == 'Eve' || x.name == 'Frank' || x.name == 'Grace' || x.name == 'Henry' " + + " SORT x.value RETURN x.value"; + var orResult = executeWithOrRule(orQuery, {}); + assertEqual(withRule, orResult, "Results with ANY == should match OR query"); + }, + + testFiresNestedAttribute: function () { + var query = "FOR x IN " + replace.name() + + " FILTER [1, 2] ANY == x.a.b SORT x.a.b RETURN x.a.b"; + + var plans = verifyExecutionPlan(query, {}); + verifyPlansDifferent(plans.withRule, plans.withoutRule, query); + + var expected = [1, 2]; + var actual = getQueryResults(query); + assertEqual(expected, actual); + + var withRule = executeWithRule(query, {}); + var withoutRule = executeWithoutRule(query, {}); + assertEqual(withRule, withoutRule, "Results with and without rule should match"); + + var orQuery = "FOR x IN " + replace.name() + + " FILTER x.a.b == 1 || x.a.b == 2 SORT x.a.b RETURN x.a.b"; + var orResult = executeWithOrRule(orQuery, {}); + assertEqual(withRule, orResult, "Results with ANY == should match OR query"); + }, + + testFiresBind: function () { + var query = + "FOR v IN " + replace.name() + + " FILTER @names ANY == v.name SORT v.value RETURN v.value"; + var params = {"names": ["Alice", "Bob"]}; + + var plans = verifyExecutionPlan(query, params); + verifyPlansDifferent(plans.withRule, plans.withoutRule, query); + + var expected = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]; + var actual = getQueryResults(query, params); + assertEqual(expected, actual); + + var withRule = executeWithRule(query, params); + var withoutRule = executeWithoutRule(query, params); + assertEqual(withRule, withoutRule, "Results with and without rule should match"); + + var orQuery = "FOR v IN " + replace.name() + + " FILTER v.name == 'Alice' || v.name == 'Bob' SORT v.value RETURN v.value"; + var orResult = executeWithOrRule(orQuery, {}); + assertEqual(withRule, orResult, "Results with ANY == should match OR query"); + }, + + testFiresVariables: function () { + var query = + "LET names = ['Alice', 'Bob'] FOR v IN " + replace.name() + + " FILTER names ANY == v.name SORT v.value RETURN v.value"; + + var plans = verifyExecutionPlan(query, {}); + verifyPlansDifferent(plans.withRule, plans.withoutRule, query); + + var expected = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]; + var actual = getQueryResults(query, {}); + assertEqual(expected, actual); + + var withRule = executeWithRule(query, {}); + var withoutRule = executeWithoutRule(query, {}); + assertEqual(withRule, withoutRule, "Results with and without rule should match"); + + var orQuery = "LET names = ['Alice', 'Bob'] FOR v IN " + replace.name() + + " FILTER v.name == 'Alice' || v.name == 'Bob' SORT v.value RETURN v.value"; + var orResult = executeWithOrRule(orQuery, {}); + assertEqual(withRule, orResult, "Results with ANY == should match OR query"); + }, + + testFiresMultipleAnyEq: function () { + var query = + "FOR v IN " + replace.name() + + " FILTER ['Alice', 'Bob'] ANY == v.name && v.value <= 20 SORT v.value RETURN v.value"; + + var plans = verifyExecutionPlan(query, {}); + verifyPlansDifferent(plans.withRule, plans.withoutRule, query); + + var expected = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]; + var actual = getQueryResults(query, {}); + assertEqual(expected, actual); + + var withRule = executeWithRule(query, {}); + var withoutRule = executeWithoutRule(query, {}); + assertEqual(withRule, withoutRule, "Results with and without rule should match"); + + var orQuery = "FOR v IN " + replace.name() + + " FILTER (v.name == 'Alice' || v.name == 'Bob') && v.value <= 20 SORT v.value RETURN v.value"; + var orResult = executeWithOrRule(orQuery, {}); + assertEqual(withRule, orResult, "Results with ANY == should match OR query"); + }, + + testFiresMultipleAnyEqDifferentAttributes: function () { + var query = + "FOR v IN " + replace.name() + + " FILTER ['Alice'] ANY == v.name && v.value <= 10 SORT v.value RETURN v.value"; + + var plans = verifyExecutionPlan(query, {}); + verifyPlansDifferent(plans.withRule, plans.withoutRule, query); + + var expected = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + var actual = getQueryResults(query, {}); + assertEqual(expected, actual); + + var withRule = executeWithRule(query, {}); + var withoutRule = executeWithoutRule(query, {}); + assertEqual(withRule, withoutRule, "Results with and without rule should match"); + + var orQuery = "FOR v IN " + replace.name() + + " FILTER v.name == 'Alice' && v.value <= 10 SORT v.value RETURN v.value"; + var orResult = executeWithOrRule(orQuery, {}); + assertEqual(withRule, orResult, "Results with ANY == should match OR query"); + }, + + testFiresNoCollection: function () { + var query = + "FOR x in 1..10 LET doc = {name: 'Alice', value: x} FILTER ['Alice', 'Bob'] " + + "ANY == doc.name SORT doc.value RETURN doc.value"; + + var plans = verifyExecutionPlan(query, {}); + verifyPlansDifferent(plans.withRule, plans.withoutRule, query); + + var expected = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + var actual = getQueryResults(query); + assertEqual(expected, actual); + + var withRule = executeWithRule(query, {}); + var withoutRule = executeWithoutRule(query, {}); + assertEqual(withRule, withoutRule, "Results with and without rule should match"); + }, + + testFiresNestedInSubquery: function () { + var query = + "FOR outer IN " + replace.name() + + " LET sub = (FOR inner IN " + replace.name() + + " FILTER ['Alice'] ANY == inner.name RETURN inner.value)" + + " FILTER ['Alice'] ANY == outer.name RETURN outer.value"; + + var plans = verifyExecutionPlan(query, {}); + verifyPlansDifferent(plans.withRule, plans.withoutRule, query); + + var expected = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + var actual = getQueryResults(query, {}); + assertEqual(expected, actual); + + var withRule = executeWithRule(query, {}); + var withoutRule = executeWithoutRule(query, {}); + assertEqual(withRule, withoutRule, "Results with and without rule should match"); + }, + + testDudNotAnyQuantifier: function () { + var query = + "FOR x IN " + replace.name() + + " FILTER ['Alice', 'Bob'] ALL == x.name RETURN x.value"; + + ruleIsNotUsed(query, {}); + }, + + testDudNoArray: function () { + var query = + "FOR x IN " + replace.name() + + " FILTER 'Alice' ANY == x.name RETURN x.value"; + + ruleIsNotUsed(query, {}); + }, + + testDudNonDeterministicArray: function () { + var query = + "FOR x IN " + replace.name() + + " FILTER NOOPT(['Alice', 'Bob']) ANY == x.name RETURN x.value"; + + ruleIsNotUsed(query, {}); + }, + + testDudNoAttribute: function () { + var query = + "FOR x IN " + replace.name() + + " FILTER ['Alice', 'Bob'] ANY == 'Alice' RETURN x.value"; + + ruleIsNotUsed(query, {}); + }, + + testDudBothArrays: function () { + var query = + "FOR x IN " + replace.name() + + " FILTER ['Alice'] ANY == ['Bob'] RETURN x.value"; + + ruleIsNotUsed(query, {}); + }, + + testDudDifferentOperators: function () { + var query = + "FOR x IN " + replace.name() + + " FILTER ['Alice', 'Bob'] ANY != x.name RETURN x.value"; + + ruleIsNotUsed(query, {}); + }, + + testIndexOptimizationWithNameIndex: function () { + var query = + "FOR x IN " + replace.name() + + " FILTER ['Alice', 'Bob'] ANY == x.name SORT x.value RETURN x.value"; + + var explainWithRule = db._createStatement({ + query: query, + bindVars: {}, + options: {optimizer: {rules: ["-all", "+replace-any-eq-with-in", "+use-indexes"]}} + }).explain(); + + var explainWithoutRule = db._createStatement({ + query: query, + bindVars: {}, + options: {optimizer: {rules: ["-all", "+use-indexes"]}} + }).explain(); + + var planWithRule = explainWithRule.plan; + var planWithoutRule = explainWithoutRule.plan; + + assertTrue(planWithRule.rules.indexOf(ruleName) !== -1, + "Plan with rule should contain replace-any-eq-with-in"); + assertTrue(planWithRule.rules.indexOf("use-indexes") !== -1, + "Plan with rule should contain use-indexes"); + + var indexNodesWith = findExecutionNodes(planWithRule, "IndexNode"); + var enumNodesWith = findExecutionNodes(planWithRule, "EnumerateCollectionNode"); + var indexNodesWithout = findExecutionNodes(planWithoutRule, "IndexNode"); + var enumNodesWithout = findExecutionNodes(planWithoutRule, "EnumerateCollectionNode"); + + assertTrue(indexNodesWith.length > 0, + "Plan with replace-any-eq-with-in should use IndexNode. " + + "Rules: " + JSON.stringify(planWithRule.rules)); + assertTrue(enumNodesWith.length === 0, + "Plan with replace-any-eq-with-in should NOT use EnumerateCollectionNode"); + + if (indexNodesWithout.length === 0) { + assertTrue(enumNodesWithout.length > 0, + "Plan without replace-any-eq-with-in should use EnumerateCollectionNode"); + } + + var expected = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]; + var withRule = executeWithRule(query, {}); + var withoutRule = executeWithoutRule(query, {}); + + assertEqual(expected, withRule); + assertEqual(withRule, withoutRule, "Results should match"); + }, + + testIndexOptimizationWithNestedAttributeIndex: function () { + var query = + "FOR x IN " + replace.name() + + " FILTER [1, 2] ANY == x.a.b SORT x.a.b RETURN x.a.b"; + + var explainWithRule = db._createStatement({ + query: query, + bindVars: {}, + options: {optimizer: {rules: ["-all", "+replace-any-eq-with-in", "+use-indexes"]}} + }).explain(); + + var planWithRule = explainWithRule.plan; + + assertTrue(planWithRule.rules.indexOf(ruleName) !== -1, + "Plan with rule should contain replace-any-eq-with-in"); + assertTrue(planWithRule.rules.indexOf("use-indexes") !== -1, + "Plan with rule should contain use-indexes"); + + var indexNodesWith = findExecutionNodes(planWithRule, "IndexNode"); + var enumNodesWith = findExecutionNodes(planWithRule, "EnumerateCollectionNode"); + + assertTrue(indexNodesWith.length > 0, + "Plan with replace-any-eq-with-in should use IndexNode for nested attribute"); + assertTrue(enumNodesWith.length === 0, + "Plan with replace-any-eq-with-in should NOT use EnumerateCollectionNode"); + + var expected = [1, 2]; + var withRule = executeWithRule(query, {}); + var withoutRule = executeWithoutRule(query, {}); + + assertEqual(expected, withRule); + assertEqual(withRule, withoutRule, "Results should match"); + }, + + testIndexOptimizationMultipleConditions: function () { + var query = + "FOR x IN " + replace.name() + + " FILTER ['Alice'] ANY == x.name && x.value <= 10 SORT x.value RETURN x.value"; + + var explainWithRule = db._createStatement({ + query: query, + bindVars: {}, + options: {optimizer: {rules: ["-all", "+replace-any-eq-with-in", "+use-indexes"]}} + }).explain(); + + var planWithRule = explainWithRule.plan; + + assertTrue(planWithRule.rules.indexOf(ruleName) !== -1, + "Plan with rule should contain replace-any-eq-with-in"); + assertTrue(planWithRule.rules.indexOf("use-indexes") !== -1, + "Plan with rule should contain use-indexes"); + + var indexNodesWith = findExecutionNodes(planWithRule, "IndexNode"); + + assertTrue(indexNodesWith.length > 0, + "Plan with replace-any-eq-with-in should use IndexNode even with multiple conditions"); + + var expected = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + var withRule = executeWithRule(query, {}); + var withoutRule = executeWithoutRule(query, {}); + + assertEqual(expected, withRule); + assertEqual(withRule, withoutRule, "Results should match"); + }, + + testFiresDuplicateValues: function () { + var query = "FOR x IN " + replace.name() + + " FILTER ['Alice', 'Alice', 'Bob', 'Bob', 'Alice'] ANY == x.name SORT x.value RETURN x.value"; + + var plans = verifyExecutionPlan(query, {}); + verifyPlansDifferent(plans.withRule, plans.withoutRule, query); + + var expected = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]; + var actual = getQueryResults(query); + assertEqual(expected, actual); + + var withRule = executeWithRule(query, {}); + var withoutRule = executeWithoutRule(query, {}); + assertEqual(withRule, withoutRule, "Results with and without rule should match"); + + var inQuery = "FOR x IN " + replace.name() + + " FILTER x.name IN ['Alice', 'Alice', 'Bob', 'Bob', 'Alice'] SORT x.value RETURN x.value"; + var inResult = executeWithOrRule(inQuery, {}); + assertEqual(withRule, inResult, "Results with ANY == should match IN query"); + }, + + testFiresEmptyString: function () { + var query = "FOR x IN " + replace.name() + + " FILTER ['', 'Alice'] ANY == x.name SORT x.value RETURN x.value"; + + var plans = verifyExecutionPlan(query, {}); + verifyPlansDifferent(plans.withRule, plans.withoutRule, query); + + var expected = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + var actual = getQueryResults(query); + assertEqual(expected, actual); + + var withRule = executeWithRule(query, {}); + var withoutRule = executeWithoutRule(query, {}); + assertEqual(withRule, withoutRule, "Results with and without rule should match"); + + var inQuery = "FOR x IN " + replace.name() + + " FILTER x.name IN ['', 'Alice'] SORT x.value RETURN x.value"; + var inResult = executeWithOrRule(inQuery, {}); + assertEqual(withRule, inResult, "Results with ANY == should match IN query"); + }, + + testFiresSpecialCharacters: function () { + var query = "FOR x IN " + replace.name() + + " FILTER ['Alice', 'O\\'Brien', 'test\"quote', 'new\\nline'] ANY == x.name SORT x.value RETURN x.value"; + + var plans = verifyExecutionPlan(query, {}); + verifyPlansDifferent(plans.withRule, plans.withoutRule, query); + + var actual = getQueryResults(query); + var withRule = executeWithRule(query, {}); + var withoutRule = executeWithoutRule(query, {}); + assertEqual(withRule, withoutRule, "Results with and without rule should match"); + + var inQuery = "FOR x IN " + replace.name() + + " FILTER x.name IN ['Alice', 'O\\'Brien', 'test\"quote', 'new\\nline'] SORT x.value RETURN x.value"; + var inResult = executeWithOrRule(inQuery, {}); + assertEqual(withRule, inResult, "Results with ANY == should match IN query"); + }, + + testFiresIndexedAccess: function () { + var query = "FOR x IN " + replace.name() + + " FILTER ['Alice', 'Bob'] ANY == x['name'] SORT x.value RETURN x.value"; + + var plans = verifyExecutionPlan(query, {}); + verifyPlansDifferent(plans.withRule, plans.withoutRule, query); + + var expected = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]; + var actual = getQueryResults(query); + assertEqual(expected, actual); + + var withRule = executeWithRule(query, {}); + var withoutRule = executeWithoutRule(query, {}); + assertEqual(withRule, withoutRule, "Results with and without rule should match"); + + var inQuery = "FOR x IN " + replace.name() + + " FILTER x['name'] IN ['Alice', 'Bob'] SORT x.value RETURN x.value"; + var inResult = executeWithOrRule(inQuery, {}); + assertEqual(withRule, inResult, "Results with ANY == should match IN query"); + }, + + testFiresEmptyArrayOptimization: function () { + var query = "FOR x IN " + replace.name() + + " FILTER [] ANY == x.name RETURN x.value"; + + var explainWithRule = db._createStatement({ + query: query, + bindVars: {}, + options: {optimizer: {rules: ["-all", "+replace-any-eq-with-in"]}} + }).explain(); + + var planWithRule = explainWithRule.plan; + var noResultNodes = findExecutionNodes(planWithRule, "NoResultsNode"); + + assertTrue(planWithRule.rules.indexOf(ruleName) !== -1, + "Plan with rule should contain replace-any-eq-with-in"); + + var expected = []; + var withRule = executeWithRule(query, {}); + var withoutRule = executeWithoutRule(query, {}); + + assertEqual(expected, withRule); + assertEqual(withRule, withoutRule, "Results with and without rule should match"); + + var inQuery = "FOR x IN " + replace.name() + + " FILTER x.name IN [] RETURN x.value"; + var inResult = executeWithOrRule(inQuery, {}); + assertEqual(withRule, inResult, "Results with ANY == should match IN query"); + }, + + testSingleValueOptimization: function () { + var query = "FOR x IN " + replace.name() + + " FILTER ['Alice'] ANY == x.name SORT x.value RETURN x.value"; + + var explainWithRule = db._createStatement({ + query: query, + bindVars: {}, + options: {optimizer: {rules: ["-all", "+replace-any-eq-with-in"]}} + }).explain(); + + var planWithRule = explainWithRule.plan; + + assertTrue(planWithRule.rules.indexOf(ruleName) !== -1, + "Plan with rule should contain replace-any-eq-with-in"); + + var expected = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + var withRule = executeWithRule(query, {}); + var withoutRule = executeWithoutRule(query, {}); + + assertEqual(expected, withRule); + assertEqual(withRule, withoutRule, "Results with and without rule should match"); + + var eqQuery = "FOR x IN " + replace.name() + + " FILTER x.name == 'Alice' SORT x.value RETURN x.value"; + var eqResult = db._query(eqQuery).toArray(); + assertEqual(withRule, eqResult, "Single-value ANY == should match == query"); + }, + + testChainedOptimizations: function () { + var query = "FOR x IN " + replace.name() + + " FILTER ['Alice', 'Alice', 'Bob'] ANY == x.name " + + " AND x.value > 5 " + + " AND x.value < 15 " + + "SORT x.value RETURN x.value"; + + var explainWithAllRules = db._createStatement({ + query: query, + bindVars: {}, + options: {optimizer: {rules: ["-all", "+replace-any-eq-with-in", "+use-indexes"]}} + }).explain(); + + var planWithAllRules = explainWithAllRules.plan; + + assertTrue(planWithAllRules.rules.indexOf(ruleName) !== -1, + "Plan should contain replace-any-eq-with-in"); + assertTrue(planWithAllRules.rules.indexOf("use-indexes") !== -1, + "Plan should contain use-indexes"); + + var indexNodes = findExecutionNodes(planWithAllRules, "IndexNode"); + assertTrue(indexNodes.length > 0, + "Should use index after ANY == transformation"); + + var expected = [6, 7, 8, 9, 10, 11, 12, 13, 14]; + var withRule = executeWithRule(query, {}); + var withoutRule = executeWithoutRule(query, {}); + + assertEqual(expected, withRule); + assertEqual(withRule, withoutRule, "Results with and without rule should match"); + }, + + testResultsMatchBetweenAnyAndIn: function () { + var testCases = [ + { + any: "['Alice'] ANY == x.name", + in: "x.name IN ['Alice']" + }, + { + any: "['Alice', 'Bob', 'Carol'] ANY == x.name", + in: "x.name IN ['Alice', 'Bob', 'Carol']" + }, + { + any: "[1, 2] ANY == x.a.b", + in: "x.a.b IN [1, 2]" + }, + { + any: "[] ANY == x.name", + in: "x.name IN []" + } + ]; + + testCases.forEach(function(testCase) { + var anyQuery = "FOR x IN " + replace.name() + + " FILTER " + testCase.any + " SORT x.value RETURN x.value"; + var inQuery = "FOR x IN " + replace.name() + + " FILTER " + testCase.in + " SORT x.value RETURN x.value"; + + var anyResult = db._query(anyQuery, {}, + {optimizer: {rules: ["-all", "+replace-any-eq-with-in"]}}).toArray(); + var inResult = db._query(inQuery, {}, + {optimizer: {rules: ["-all"]}}).toArray(); + + assertEqual(anyResult, inResult, + "ANY == and IN should produce same results for: " + testCase.any); + }); + }, + + testIndexUsageComparison: function () { + var anyQuery = "FOR x IN " + replace.name() + + " FILTER ['Alice', 'Bob'] ANY == x.name RETURN x"; + var inQuery = "FOR x IN " + replace.name() + + " FILTER x.name IN ['Alice', 'Bob'] RETURN x"; + + var anyPlan = db._createStatement({ + query: anyQuery, + bindVars: {}, + options: {optimizer: {rules: ["-all", "+replace-any-eq-with-in", "+use-indexes"]}} + }).explain(); + + var inPlan = db._createStatement({ + query: inQuery, + bindVars: {}, + options: {optimizer: {rules: ["-all", "+use-indexes"]}} + }).explain(); + + var anyIndexNodes = findExecutionNodes(anyPlan.plan, "IndexNode"); + var inIndexNodes = findExecutionNodes(inPlan.plan, "IndexNode"); + + assertTrue(anyIndexNodes.length > 0, + "ANY == (transformed to IN) should use index"); + assertTrue(inIndexNodes.length > 0, + "IN should use index"); + + var anyResult = db._query(anyQuery, {}, + {optimizer: {rules: ["-all", "+replace-any-eq-with-in", "+use-indexes"]}}).toArray(); + var inResult = db._query(inQuery, {}, + {optimizer: {rules: ["-all", "+use-indexes"]}}).toArray(); + + assertEqual(anyResult.length, inResult.length, + "ANY == and IN should return same number of results"); + } + }; +} + +jsunity.run(NewAqlReplaceAnyWithINTestSuite); + +return jsunity.done(); +