1
0
Fork 0

k Shortest Paths (#8715)

* Add k-shortest-paths finder
This commit is contained in:
Markus Pfeiffer 2019-04-12 20:58:18 +01:00 committed by Michael Hackstein
parent b76f8adc0f
commit a14386267a
40 changed files with 4965 additions and 2050 deletions

View File

@ -1358,6 +1358,33 @@ AstNode* Ast::createNodeShortestPath(AstNode const* outVars, AstNode const* grap
return node;
}
/// @brief create an AST k-shortest paths node
AstNode* Ast::createNodeKShortestPaths(AstNode const* outVars, AstNode const* graphInfo) {
TRI_ASSERT(outVars->type == NODE_TYPE_ARRAY);
TRI_ASSERT(graphInfo->type == NODE_TYPE_ARRAY);
AstNode* node = createNode(NODE_TYPE_K_SHORTEST_PATHS);
node->reserve(outVars->numMembers() + graphInfo->numMembers());
TRI_ASSERT(graphInfo->numMembers() == 5);
TRI_ASSERT(outVars->numMembers() > 0);
TRI_ASSERT(outVars->numMembers() < 3);
// Add GraphInfo
for (size_t i = 0; i < graphInfo->numMembers(); ++i) {
node->addMember(graphInfo->getMemberUnchecked(i));
}
// Add variables
for (size_t i = 0; i < outVars->numMembers(); ++i) {
node->addMember(outVars->getMemberUnchecked(i));
}
TRI_ASSERT(node->numMembers() == graphInfo->numMembers() + outVars->numMembers());
_containsTraversal = true;
return node;
}
AstNode const* Ast::createNodeOptions(AstNode const* options) const {
if (options != nullptr) {
return options;

View File

@ -366,6 +366,9 @@ class Ast {
/// @brief create an AST shortest path node
AstNode* createNodeShortestPath(AstNode const*, AstNode const*);
/// @brief create an AST k-shortest paths node
AstNode* createNodeKShortestPaths(AstNode const*, AstNode const*);
/// @brief create an AST function call node
AstNode* createNodeFunctionCall(char const* functionName, AstNode const* arguments) {
return createNodeFunctionCall(functionName, strlen(functionName), arguments);

View File

@ -158,6 +158,7 @@ std::unordered_map<int, std::string const> const AstNode::TypeNames{
"array compare not in"},
{static_cast<int>(NODE_TYPE_QUANTIFIER), "quantifier"},
{static_cast<int>(NODE_TYPE_SHORTEST_PATH), "shortest path"},
{static_cast<int>(NODE_TYPE_K_SHORTEST_PATHS), "k-shortest paths"},
{static_cast<int>(NODE_TYPE_VIEW), "view"},
{static_cast<int>(NODE_TYPE_PARAMETER_DATASOURCE), "datasource parameter"},
{static_cast<int>(NODE_TYPE_FOR_VIEW), "view enumeration"},
@ -570,6 +571,7 @@ AstNode::AstNode(Ast* ast, arangodb::velocypack::Slice const& slice)
case NODE_TYPE_DISTINCT:
case NODE_TYPE_TRAVERSAL:
case NODE_TYPE_SHORTEST_PATH:
case NODE_TYPE_K_SHORTEST_PATHS:
case NODE_TYPE_DIRECTION:
case NODE_TYPE_COLLECTION_LIST:
case NODE_TYPE_OPERATOR_NARY_AND:
@ -690,6 +692,7 @@ AstNode::AstNode(std::function<void(AstNode*)> const& registerNode,
case NODE_TYPE_DISTINCT:
case NODE_TYPE_TRAVERSAL:
case NODE_TYPE_SHORTEST_PATH:
case NODE_TYPE_K_SHORTEST_PATHS:
case NODE_TYPE_DIRECTION:
case NODE_TYPE_COLLECTION_LIST:
case NODE_TYPE_PASSTHRU:
@ -2334,6 +2337,7 @@ void AstNode::findVariableAccess(std::vector<AstNode const*>& currentPath,
case NODE_TYPE_DISTINCT:
case NODE_TYPE_TRAVERSAL:
case NODE_TYPE_SHORTEST_PATH:
case NODE_TYPE_K_SHORTEST_PATHS:
case NODE_TYPE_COLLECTION_LIST:
case NODE_TYPE_DIRECTION:
case NODE_TYPE_WITH:
@ -2507,6 +2511,7 @@ AstNode const* AstNode::findReference(AstNode const* findme) const {
case NODE_TYPE_DISTINCT:
case NODE_TYPE_TRAVERSAL:
case NODE_TYPE_SHORTEST_PATH:
case NODE_TYPE_K_SHORTEST_PATHS:
case NODE_TYPE_COLLECTION_LIST:
case NODE_TYPE_DIRECTION:
case NODE_TYPE_OPERATOR_NARY_AND:

View File

@ -204,9 +204,10 @@ enum AstNodeType : uint32_t {
NODE_TYPE_QUANTIFIER = 73,
NODE_TYPE_WITH = 74,
NODE_TYPE_SHORTEST_PATH = 75,
NODE_TYPE_VIEW = 76,
NODE_TYPE_PARAMETER_DATASOURCE = 77,
NODE_TYPE_FOR_VIEW = 78,
NODE_TYPE_K_SHORTEST_PATHS = 76,
NODE_TYPE_VIEW = 77,
NODE_TYPE_PARAMETER_DATASOURCE = 78,
NODE_TYPE_FOR_VIEW = 79,
};
static_assert(NODE_TYPE_VALUE < NODE_TYPE_ARRAY, "incorrect node types order");

View File

@ -42,6 +42,7 @@ bool ConditionFinder::before(ExecutionNode* en) {
case EN::INDEX:
case EN::RETURN:
case EN::TRAVERSAL:
case EN::K_SHORTEST_PATHS:
case EN::SHORTEST_PATH:
case EN::ENUMERATE_IRESEARCH_VIEW:
{

View File

@ -43,6 +43,7 @@
#include "Aql/IResearchViewExecutor.h"
#include "Aql/IdExecutor.h"
#include "Aql/IndexExecutor.h"
#include "Aql/KShortestPathsExecutor.h"
#include "Aql/LimitExecutor.h"
#include "Aql/ModificationExecutor.h"
#include "Aql/ModificationExecutorTraits.h"
@ -298,6 +299,19 @@ std::pair<ExecutionState, Result> ExecutionBlockImpl<ShortestPathExecutor>::shut
return this->executor().shutdown(errorCode);
}
template <>
std::pair<ExecutionState, Result> ExecutionBlockImpl<KShortestPathsExecutor>::shutdown(int errorCode) {
ExecutionState state;
Result result;
std::tie(state, result) = ExecutionBlock::shutdown(errorCode);
if (state == ExecutionState::WAITING) {
return {state, result};
}
return this->executor().shutdown(errorCode);
}
template <>
std::pair<ExecutionState, Result> ExecutionBlockImpl<SubqueryExecutor>::shutdown(int errorCode) {
ExecutionState state;
@ -422,6 +436,7 @@ template class ::arangodb::aql::ExecutionBlockImpl<NoResultsExecutor>;
template class ::arangodb::aql::ExecutionBlockImpl<ReturnExecutor<false>>;
template class ::arangodb::aql::ExecutionBlockImpl<ReturnExecutor<true>>;
template class ::arangodb::aql::ExecutionBlockImpl<ShortestPathExecutor>;
template class ::arangodb::aql::ExecutionBlockImpl<KShortestPathsExecutor>;
template class ::arangodb::aql::ExecutionBlockImpl<SortedCollectExecutor>;
template class ::arangodb::aql::ExecutionBlockImpl<SortExecutor>;
template class ::arangodb::aql::ExecutionBlockImpl<SubqueryExecutor>;

View File

@ -369,6 +369,7 @@ struct CoordinatorInstanciator final : public WalkerWorker<ExecutionNode> {
break;
case ExecutionNode::TRAVERSAL:
case ExecutionNode::SHORTEST_PATH:
case ExecutionNode::K_SHORTEST_PATHS:
_dbserverParts.addGraphNode(ExecutionNode::castTo<GraphNode*>(en));
break;
default:

View File

@ -42,6 +42,7 @@
#include "Aql/IResearchViewNode.h"
#include "Aql/IdExecutor.h"
#include "Aql/IndexNode.h"
#include "Aql/KShortestPathsNode.h"
#include "Aql/LimitExecutor.h"
#include "Aql/ModificationNodes.h"
#include "Aql/NoResultsExecutor.h"
@ -94,6 +95,7 @@ std::unordered_map<int, std::string const> const typeNames{
{static_cast<int>(ExecutionNode::UPSERT), "UpsertNode"},
{static_cast<int>(ExecutionNode::TRAVERSAL), "TraversalNode"},
{static_cast<int>(ExecutionNode::SHORTEST_PATH), "ShortestPathNode"},
{static_cast<int>(ExecutionNode::K_SHORTEST_PATHS), "KShortestPathsNode"},
{static_cast<int>(ExecutionNode::REMOTESINGLE),
"SingleRemoteOperationNode"},
{static_cast<int>(ExecutionNode::ENUMERATE_IRESEARCH_VIEW),
@ -322,6 +324,8 @@ ExecutionNode* ExecutionNode::fromVPackFactory(ExecutionPlan* plan, VPackSlice c
return new TraversalNode(plan, slice);
case SHORTEST_PATH:
return new ShortestPathNode(plan, slice);
case K_SHORTEST_PATHS:
return new KShortestPathsNode(plan, slice);
case REMOTESINGLE:
return new SingleRemoteOperationNode(plan, slice);
case ENUMERATE_IRESEARCH_VIEW:
@ -1011,7 +1015,8 @@ void ExecutionNode::RegisterPlan::after(ExecutionNode* en) {
}
case ExecutionNode::TRAVERSAL:
case ExecutionNode::SHORTEST_PATH: {
case ExecutionNode::SHORTEST_PATH:
case ExecutionNode::K_SHORTEST_PATHS: {
depth++;
auto ep = dynamic_cast<GraphNode const*>(en);
if (ep == nullptr) {

View File

@ -144,7 +144,8 @@ class ExecutionNode {
TRAVERSAL = 22,
INDEX = 23,
SHORTEST_PATH = 24,
REMOTESINGLE = 25,
K_SHORTEST_PATHS = 25,
REMOTESINGLE = 26,
ENUMERATE_IRESEARCH_VIEW,
MAX_NODE_TYPE_VALUE
};

View File

@ -32,6 +32,7 @@
#include "Aql/Function.h"
#include "Aql/IResearchViewNode.h"
#include "Aql/IndexHint.h"
#include "Aql/KShortestPathsNode.h"
#include "Aql/ModificationNodes.h"
#include "Aql/NodeFinder.h"
#include "Aql/OptimizerRulesFeature.h"
@ -1121,6 +1122,40 @@ ExecutionNode* ExecutionPlan::fromNodeShortestPath(ExecutionNode* previous,
return addDependency(previous, en);
}
/// @brief create an execution plan element from an AST for SHORTEST_PATH node
ExecutionNode* ExecutionPlan::fromNodeKShortestPaths(ExecutionNode* previous,
AstNode const* node) {
TRI_ASSERT(node != nullptr && node->type == NODE_TYPE_K_SHORTEST_PATHS);
TRI_ASSERT(node->numMembers() >= 6);
TRI_ASSERT(node->numMembers() <= 7);
// the first 4 members are used by shortest_path internally.
// The members 5-6, where 6 is optional, are used
// as out variables.
AstNode const* direction = node->getMember(0);
TRI_ASSERT(direction->isIntValue());
AstNode const* start = parseTraversalVertexNode(previous, node->getMember(1));
AstNode const* target = parseTraversalVertexNode(previous, node->getMember(2));
AstNode const* graph = node->getMember(3);
// FIXME: here goes the parameters with k etc
auto options = createShortestPathOptions(getAst()->query(), node->getMember(4));
// First create the node
auto spNode = new KShortestPathsNode(this, nextId(), &(_ast->query()->vocbase()), direction,
start, target, graph, std::move(options));
auto variable = node->getMember(5);
TRI_ASSERT(variable->type == NODE_TYPE_VARIABLE);
auto v = static_cast<Variable*>(variable->getData());
TRI_ASSERT(v != nullptr);
spNode->setPathOutput(v);
ExecutionNode* en = registerNode(spNode);
TRI_ASSERT(en != nullptr);
return addDependency(previous, en);
}
/// @brief create an execution plan element from an AST FILTER node
ExecutionNode* ExecutionPlan::fromNodeFilter(ExecutionNode* previous, AstNode const* node) {
TRI_ASSERT(node != nullptr);
@ -1881,6 +1916,10 @@ ExecutionNode* ExecutionPlan::fromNode(AstNode const* node) {
break;
}
case NODE_TYPE_K_SHORTEST_PATHS: {
en = fromNodeKShortestPaths(en, member);
break;
}
case NODE_TYPE_FILTER: {
en = fromNodeFilter(en, member);
break;

View File

@ -286,6 +286,9 @@ class ExecutionPlan {
/// @brief create an execution plan element from an AST SHORTEST PATH node
ExecutionNode* fromNodeShortestPath(ExecutionNode*, AstNode const*);
/// @brief create an execution plan element from an AST K-SHORTEST PATHS node
ExecutionNode* fromNodeKShortestPaths(ExecutionNode*, AstNode const*);
/// @brief create an execution plan element from an AST FILTER node
ExecutionNode* fromNodeFilter(ExecutionNode*, AstNode const*);

View File

@ -0,0 +1,254 @@
////////////////////////////////////////////////////////////////////////////////
/// DISCLAIMER
///
/// Copyright 2019 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 Markus Pfeiffer
////////////////////////////////////////////////////////////////////////////////
#include "KShortestPathsExecutor.h"
#include "Aql/AqlValue.h"
#include "Aql/OutputAqlItemRow.h"
#include "Aql/Query.h"
#include "Aql/SingleRowFetcher.h"
#include "Aql/Stats.h"
#include "Graph/KShortestPathsFinder.h"
#include "Graph/ShortestPathOptions.h"
#include "Graph/ShortestPathResult.h"
#include "Transaction/Helpers.h"
#include <velocypack/Builder.h>
#include <velocypack/Slice.h>
#include <velocypack/StringRef.h>
#include <velocypack/velocypack-aliases.h>
#include <utility>
using namespace arangodb;
using namespace arangodb::aql;
using namespace arangodb::graph;
namespace {
static bool isValidId(VPackSlice id) {
TRI_ASSERT(id.isString());
arangodb::velocypack::StringRef tester(id);
return tester.find('/') != std::string::npos;
}
static RegisterId selectOutputRegister(
std::shared_ptr<std::unordered_set<RegisterId> const> const& outputRegisters) {
TRI_ASSERT(outputRegisters != nullptr);
TRI_ASSERT(outputRegisters->size() == 1);
RegisterId reg = *(outputRegisters->begin());
TRI_ASSERT(reg != ExecutionNode::MaxRegisterId);
return reg;
}
} // namespace
KShortestPathsExecutorInfos::KShortestPathsExecutorInfos(
std::shared_ptr<std::unordered_set<RegisterId>> inputRegisters,
std::shared_ptr<std::unordered_set<RegisterId>> outputRegisters, RegisterId nrInputRegisters,
RegisterId nrOutputRegisters, std::unordered_set<RegisterId> registersToClear,
std::unordered_set<RegisterId> registersToKeep,
std::unique_ptr<graph::KShortestPathsFinder>&& finder,
InputVertex&& source, InputVertex&& target)
: ExecutorInfos(std::move(inputRegisters), std::move(outputRegisters),
nrInputRegisters, nrOutputRegisters,
std::move(registersToClear), std::move(registersToKeep)),
_finder(std::move(finder)),
_source(std::move(source)),
_target(std::move(target)),
_outputRegister(::selectOutputRegister(_outRegs)) {}
KShortestPathsExecutorInfos::KShortestPathsExecutorInfos(KShortestPathsExecutorInfos&&) = default;
KShortestPathsExecutorInfos::~KShortestPathsExecutorInfos() = default;
arangodb::graph::KShortestPathsFinder& KShortestPathsExecutorInfos::finder() const {
TRI_ASSERT(_finder);
return *_finder.get();
}
bool KShortestPathsExecutorInfos::useRegisterForInput(bool isTarget) const {
if (isTarget) {
return _target.type == InputVertex::REGISTER;
}
return _source.type == InputVertex::REGISTER;
}
RegisterId KShortestPathsExecutorInfos::getInputRegister(bool isTarget) const {
TRI_ASSERT(useRegisterForInput(isTarget));
if (isTarget) {
return _target.reg;
}
return _source.reg;
}
std::string const& KShortestPathsExecutorInfos::getInputValue(bool isTarget) const {
TRI_ASSERT(!useRegisterForInput(isTarget));
if (isTarget) {
return _target.value;
}
return _source.value;
}
RegisterId KShortestPathsExecutorInfos::getOutputRegister() const {
TRI_ASSERT(_outputRegister != ExecutionNode::MaxRegisterId);
return _outputRegister;
}
graph::TraverserCache* KShortestPathsExecutorInfos::cache() const {
return _finder->options().cache();
}
KShortestPathsExecutor::KShortestPathsExecutor(Fetcher& fetcher, Infos& infos)
: _infos(infos),
_fetcher(fetcher),
_input{CreateInvalidInputRowHint{}},
_rowState(ExecutionState::HASMORE),
_finder{infos.finder()},
_sourceBuilder{},
_targetBuilder{} {
if (!_infos.useRegisterForInput(false)) {
_sourceBuilder.add(VPackValue(_infos.getInputValue(false)));
}
if (!_infos.useRegisterForInput(true)) {
_targetBuilder.add(VPackValue(_infos.getInputValue(true)));
}
}
KShortestPathsExecutor::~KShortestPathsExecutor() = default;
// Shutdown query
std::pair<ExecutionState, Result> KShortestPathsExecutor::shutdown(int errorCode) {
_finder.destroyEngines();
return {ExecutionState::DONE, TRI_ERROR_NO_ERROR};
}
std::pair<ExecutionState, NoStats> KShortestPathsExecutor::produceRow(OutputAqlItemRow& output) {
NoStats s;
while (true) {
// We will have paths available, or return
if (!_finder.isPathAvailable()) {
if (!fetchPaths()) {
return {_rowState, s};
}
}
// Now we have a path available, so we go and output it
transaction::BuilderLeaser tmp(_finder.options().trx());
tmp->clear();
if (_finder.getNextPathAql(*tmp.builder())) {
AqlValue path = AqlValue(*tmp.builder());
AqlValueGuard guard{path, true};
output.moveValueInto(_infos.getOutputRegister(), _input, guard);
return {computeState(), s};
}
}
}
bool KShortestPathsExecutor::fetchPaths() {
VPackSlice start;
VPackSlice end;
do {
// Fetch a row from upstream
std::tie(_rowState, _input) = _fetcher.fetchRow();
if (!_input.isInitialized()) {
// Either WAITING or DONE, in either case we cannot produce any paths.
TRI_ASSERT(_rowState == ExecutionState::WAITING || _rowState == ExecutionState::DONE);
return false;
}
// Check start and end for validity
if (!getVertexId(false, start) || !getVertexId(true, end)) {
// Fetch another row
continue;
}
TRI_ASSERT(start.isString());
TRI_ASSERT(end.isString());
} while (!_finder.startKShortestPathsTraversal(start, end));
return true;
}
ExecutionState KShortestPathsExecutor::computeState() const {
if (_rowState == ExecutionState::HASMORE || _finder.isPathAvailable()) {
return ExecutionState::HASMORE;
}
return ExecutionState::DONE;
}
bool KShortestPathsExecutor::getVertexId(bool isTarget, VPackSlice& id) {
if (_infos.useRegisterForInput(isTarget)) {
// The input row stays valid until the next fetchRow is executed.
// So the slice can easily point to it.
RegisterId reg = _infos.getInputRegister(isTarget);
AqlValue const& in = _input.getValue(reg);
if (in.isObject()) {
try {
auto idString = _finder.options().trx()->extractIdString(in.slice());
if (isTarget) {
_targetBuilder.clear();
_targetBuilder.add(VPackValue(idString));
id = _targetBuilder.slice();
} else {
_sourceBuilder.clear();
_sourceBuilder.add(VPackValue(idString));
id = _sourceBuilder.slice();
}
// Guranteed by extractIdValue
TRI_ASSERT(::isValidId(id));
} catch (...) {
// _id or _key not present... ignore this error and fall through
// returning no path
return false;
}
return true;
} else if (in.isString()) {
id = in.slice();
// Validation
if (!::isValidId(id)) {
_finder.options().query()->registerWarning(
TRI_ERROR_BAD_PARAMETER,
"Invalid input for k Shortest Paths: "
"Only id strings or objects with "
"_id are allowed");
return false;
}
return true;
} else {
_finder.options().query()->registerWarning(
TRI_ERROR_BAD_PARAMETER,
"Invalid input for k Shortest Paths: "
"Only id strings or objects with "
"_id are allowed");
return false;
}
} else {
if (isTarget) {
id = _targetBuilder.slice();
} else {
id = _sourceBuilder.slice();
}
if (!::isValidId(id)) {
_finder.options().query()->registerWarning(
TRI_ERROR_BAD_PARAMETER,
"Invalid input for k Shortest Paths: "
"Only id strings or objects with "
"_id are allowed");
return false;
}
return true;
}
}

View File

@ -0,0 +1,203 @@
////////////////////////////////////////////////////////////////////////////////
/// DISCLAIMER
///
/// Copyright 2019 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 Markus Pfeiffer
////////////////////////////////////////////////////////////////////////////////
#ifndef ARANGOD_AQL_KSHORTEST_PATHS_EXECUTOR_H
#define ARANGOD_AQL_KSHORTEST_PATHS_EXECUTOR_H
#include "Aql/ExecutionState.h"
#include "Aql/ExecutorInfos.h"
#include "Aql/InputAqlItemRow.h"
#include <velocypack/Builder.h>
namespace arangodb {
class Result;
namespace velocypack {
class Slice;
}
namespace graph {
class KShortestPathsFinder;
class ShortestPathFinder;
class ShortestPathResult;
class TraverserCache;
} // namespace graph
namespace aql {
template <bool>
class SingleRowFetcher;
class OutputAqlItemRow;
class NoStats;
class KShortestPathsExecutorInfos : public ExecutorInfos {
public:
struct InputVertex {
enum { CONSTANT, REGISTER } type;
// TODO make the following two a union instead
RegisterId reg;
std::string value;
// cppcheck-suppress passedByValue
explicit InputVertex(std::string value)
: type(CONSTANT), reg(0), value(std::move(value)) {}
explicit InputVertex(RegisterId reg)
: type(REGISTER), reg(reg), value("") {}
};
KShortestPathsExecutorInfos(std::shared_ptr<std::unordered_set<RegisterId>> inputRegisters,
std::shared_ptr<std::unordered_set<RegisterId>> outputRegisters,
RegisterId nrInputRegisters, RegisterId nrOutputRegisters,
std::unordered_set<RegisterId> registersToClear,
std::unordered_set<RegisterId> registersToKeep,
std::unique_ptr<graph::KShortestPathsFinder>&& finder,
InputVertex&& source, InputVertex&& target);
KShortestPathsExecutorInfos() = delete;
KShortestPathsExecutorInfos(KShortestPathsExecutorInfos&&);
KShortestPathsExecutorInfos(KShortestPathsExecutorInfos const&) = delete;
~KShortestPathsExecutorInfos();
arangodb::graph::KShortestPathsFinder& finder() const;
/**
* @brief test if we use a register or a constant input
*
* @param isTarget defines if we look for target(true) or source(false)
*/
bool useRegisterForInput(bool isTarget) const;
/**
* @brief get the register used for the input
*
* @param isTarget defines if we look for target(true) or source(false)
*/
RegisterId getInputRegister(bool isTarget) const;
/**
* @brief get the const value for the input
*
* @param isTarget defines if we look for target(true) or source(false)
*/
std::string const& getInputValue(bool isTarget) const;
/**
* @brief get the output register for the given type
*/
RegisterId getOutputRegister() const;
graph::TraverserCache* cache() const;
private:
/// @brief the shortest path finder.
std::unique_ptr<arangodb::graph::KShortestPathsFinder> _finder;
/// @brief Information about the source vertex
InputVertex const _source;
/// @brief Information about the target vertex
InputVertex const _target;
RegisterId const _outputRegister;
};
/**
* @brief Implementation of ShortestPath Node
*/
class KShortestPathsExecutor {
public:
struct Properties {
static const bool preservesOrder = true;
static const bool allowsBlockPassthrough = false;
static const bool inputSizeRestrictsOutputSize = false;
};
using Fetcher = SingleRowFetcher<Properties::allowsBlockPassthrough>;
using Infos = KShortestPathsExecutorInfos;
using Stats = NoStats;
KShortestPathsExecutor() = delete;
KShortestPathsExecutor(KShortestPathsExecutor&&) = default;
KShortestPathsExecutor(Fetcher& fetcher, Infos&);
~KShortestPathsExecutor();
/**
* @brief Shutdown will be called once for every query
*
* @return ExecutionState and no error.
*/
std::pair<ExecutionState, Result> shutdown(int errorCode);
/**
* @brief produce the next Row of Aql Values.
*
* @return ExecutionState, and if successful exactly one new Row of AqlItems.
*/
std::pair<ExecutionState, Stats> produceRow(OutputAqlItemRow& output);
inline std::pair<ExecutionState, size_t> expectedNumberOfRows(size_t) const {
TRI_ASSERT(false);
THROW_ARANGO_EXCEPTION_MESSAGE(
TRI_ERROR_INTERNAL,
"Logic_error, prefetching number fo rows not supported");
}
private:
/**
* @brief Fetch input row(s) and compute path
*
* @return false if we are done and no path could be found.
*/
bool fetchPaths();
/**
* @brief compute the correct return state
*
* @return DONE if no more is expected
*/
ExecutionState computeState() const;
/**
* @brief get the id of a input vertex
*/
bool getVertexId(bool isTarget, arangodb::velocypack::Slice& id);
private:
Infos& _infos;
Fetcher& _fetcher;
InputAqlItemRow _input;
ExecutionState _rowState;
/// @brief the shortest path finder.
arangodb::graph::KShortestPathsFinder& _finder;
/// @brief temporary memory mangement for source id
arangodb::velocypack::Builder _sourceBuilder;
/// @brief temporary memory mangement for target id
arangodb::velocypack::Builder _targetBuilder;
};
} // namespace aql
} // namespace arangodb
#endif

View File

@ -0,0 +1,359 @@
/// @brief Implementation of k Shortest Path Execution Node
///
/// @file arangod/Aql/KShortestPathsNode.cpp
///
/// DISCLAIMER
///
/// Copyright 2010-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 Michael Hackstein
/// @author Markus Pfeiffer
/// @author Copyright 2015, ArangoDB GmbH, Cologne, Germany
////////////////////////////////////////////////////////////////////////////////
#include "KShortestPathsNode.h"
#include "Aql/Ast.h"
#include "Aql/Collection.h"
#include "Aql/ExecutionBlockImpl.h"
#include "Aql/ExecutionPlan.h"
#include "Aql/KShortestPathsExecutor.h"
#include "Aql/Query.h"
#include "Graph/AttributeWeightShortestPathFinder.h"
#include "Graph/KShortestPathsFinder.h"
#include "Graph/ShortestPathFinder.h"
#include "Graph/ShortestPathOptions.h"
#include "Graph/ShortestPathResult.h"
#include "Indexes/Index.h"
#include "Utils/CollectionNameResolver.h"
#include "VocBase/LogicalCollection.h"
#include <velocypack/Iterator.h>
#include <velocypack/velocypack-aliases.h>
using namespace arangodb;
using namespace arangodb::basics;
using namespace arangodb::aql;
using namespace arangodb::graph;
namespace {
static void parseNodeInput(AstNode const* node, std::string& id, Variable const*& variable) {
switch (node->type) {
case NODE_TYPE_REFERENCE:
variable = static_cast<Variable*>(node->getData());
id = "";
break;
case NODE_TYPE_VALUE:
if (node->value.type != VALUE_TYPE_STRING) {
THROW_ARANGO_EXCEPTION_MESSAGE(TRI_ERROR_QUERY_PARSE,
"invalid start vertex. Must either be "
"an _id string or an object with _id.");
}
variable = nullptr;
id = node->getString();
break;
default:
THROW_ARANGO_EXCEPTION_MESSAGE(TRI_ERROR_QUERY_PARSE,
"invalid start vertex. Must either be an "
"_id string or an object with _id.");
}
}
static KShortestPathsExecutorInfos::InputVertex prepareVertexInput(KShortestPathsNode const* node,
bool isTarget) {
using InputVertex = KShortestPathsExecutorInfos::InputVertex;
if (isTarget) {
if (node->usesTargetInVariable()) {
auto it = node->getRegisterPlan()->varInfo.find(node->targetInVariable().id);
TRI_ASSERT(it != node->getRegisterPlan()->varInfo.end());
return InputVertex{it->second.registerId};
} else {
return InputVertex{node->getTargetVertex()};
}
} else {
if (node->usesStartInVariable()) {
auto it = node->getRegisterPlan()->varInfo.find(node->startInVariable().id);
TRI_ASSERT(it != node->getRegisterPlan()->varInfo.end());
return InputVertex{it->second.registerId};
} else {
return InputVertex{node->getStartVertex()};
}
}
}
} // namespace
KShortestPathsNode::KShortestPathsNode(ExecutionPlan* plan, size_t id, TRI_vocbase_t* vocbase,
AstNode const* direction, AstNode const* start,
AstNode const* target, AstNode const* graph,
std::unique_ptr<BaseOptions> options)
: GraphNode(plan, id, vocbase, direction, graph, std::move(options)),
_pathOutVariable(nullptr),
_inStartVariable(nullptr),
_inTargetVariable(nullptr),
_fromCondition(nullptr),
_toCondition(nullptr) {
TRI_ASSERT(start != nullptr);
TRI_ASSERT(target != nullptr);
TRI_ASSERT(graph != nullptr);
auto ast = _plan->getAst();
// Let us build the conditions on _from and _to. Just in case we need them.
{
auto const* access =
ast->createNodeAttributeAccess(getTemporaryRefNode(),
StaticStrings::FromString.c_str(),
StaticStrings::FromString.length());
auto const* cond =
ast->createNodeBinaryOperator(NODE_TYPE_OPERATOR_BINARY_EQ, access, _tmpIdNode);
_fromCondition = ast->createNodeNaryOperator(NODE_TYPE_OPERATOR_NARY_AND);
_fromCondition->addMember(cond);
}
TRI_ASSERT(_fromCondition != nullptr);
{
auto const* access =
ast->createNodeAttributeAccess(getTemporaryRefNode(),
StaticStrings::ToString.c_str(),
StaticStrings::ToString.length());
auto const* cond =
ast->createNodeBinaryOperator(NODE_TYPE_OPERATOR_BINARY_EQ, access, _tmpIdNode);
_toCondition = ast->createNodeNaryOperator(NODE_TYPE_OPERATOR_NARY_AND);
_toCondition->addMember(cond);
}
TRI_ASSERT(_toCondition != nullptr);
parseNodeInput(start, _startVertexId, _inStartVariable);
parseNodeInput(target, _targetVertexId, _inTargetVariable);
}
/// @brief Internal constructor to clone the node.
KShortestPathsNode::KShortestPathsNode(
ExecutionPlan* plan, size_t id, TRI_vocbase_t* vocbase,
std::vector<std::unique_ptr<Collection>> const& edgeColls,
std::vector<std::unique_ptr<Collection>> const& vertexColls,
std::vector<TRI_edge_direction_e> const& directions, Variable const* inStartVariable,
std::string const& startVertexId, Variable const* inTargetVariable,
std::string const& targetVertexId, std::unique_ptr<BaseOptions> options)
: GraphNode(plan, id, vocbase, edgeColls, vertexColls, directions, std::move(options)),
_pathOutVariable(nullptr),
_inStartVariable(inStartVariable),
_startVertexId(startVertexId),
_inTargetVariable(inTargetVariable),
_targetVertexId(targetVertexId),
_fromCondition(nullptr),
_toCondition(nullptr) {}
KShortestPathsNode::~KShortestPathsNode() {}
KShortestPathsNode::KShortestPathsNode(ExecutionPlan* plan,
arangodb::velocypack::Slice const& base)
: GraphNode(plan, base),
_pathOutVariable(nullptr),
_inStartVariable(nullptr),
_inTargetVariable(nullptr),
_fromCondition(nullptr),
_toCondition(nullptr) {
// Path out variable
if (base.hasKey("pathOutVariable")) {
_pathOutVariable =
Variable::varFromVPack(plan->getAst(), base, "pathOutVariable");
}
// Start Vertex
if (base.hasKey("startInVariable")) {
_inStartVariable =
Variable::varFromVPack(plan->getAst(), base, "startInVariable");
} else {
VPackSlice v = base.get("startVertexId");
if (!v.isString()) {
THROW_ARANGO_EXCEPTION_MESSAGE(TRI_ERROR_QUERY_BAD_JSON_PLAN,
"start vertex must be a string");
}
_startVertexId = v.copyString();
if (_startVertexId.empty()) {
THROW_ARANGO_EXCEPTION_MESSAGE(TRI_ERROR_QUERY_BAD_JSON_PLAN,
"start vertex mustn't be empty");
}
}
// Target Vertex
if (base.hasKey("targetInVariable")) {
_inTargetVariable =
Variable::varFromVPack(plan->getAst(), base, "targetInVariable");
} else {
VPackSlice v = base.get("targetVertexId");
if (!v.isString()) {
THROW_ARANGO_EXCEPTION_MESSAGE(TRI_ERROR_QUERY_BAD_JSON_PLAN,
"target vertex must be a string");
}
_targetVertexId = v.copyString();
if (_targetVertexId.empty()) {
THROW_ARANGO_EXCEPTION_MESSAGE(TRI_ERROR_QUERY_BAD_JSON_PLAN,
"target vertex mustn't be empty");
}
}
// Filter Condition Parts
TRI_ASSERT(base.hasKey("fromCondition"));
_fromCondition = new AstNode(plan->getAst(), base.get("fromCondition"));
TRI_ASSERT(base.hasKey("toCondition"));
_toCondition = new AstNode(plan->getAst(), base.get("toCondition"));
}
void KShortestPathsNode::toVelocyPackHelper(VPackBuilder& nodes, unsigned flags) const {
GraphNode::toVelocyPackHelper(nodes, flags); // call base class method
// Out variables
if (usesPathOutVariable()) {
nodes.add(VPackValue("pathOutVariable"));
pathOutVariable().toVelocyPack(nodes);
}
// In variables
if (usesStartInVariable()) {
nodes.add(VPackValue("startInVariable"));
startInVariable().toVelocyPack(nodes);
} else {
nodes.add("startVertexId", VPackValue(_startVertexId));
}
if (usesTargetInVariable()) {
nodes.add(VPackValue("targetInVariable"));
targetInVariable().toVelocyPack(nodes);
} else {
nodes.add("targetVertexId", VPackValue(_targetVertexId));
}
// Filter Conditions
TRI_ASSERT(_fromCondition != nullptr);
nodes.add(VPackValue("fromCondition"));
_fromCondition->toVelocyPack(nodes, flags);
TRI_ASSERT(_toCondition != nullptr);
nodes.add(VPackValue("toCondition"));
_toCondition->toVelocyPack(nodes, flags);
// And close it:
nodes.close();
}
/// @brief creates corresponding ExecutionBlock
std::unique_ptr<ExecutionBlock> KShortestPathsNode::createBlock(
ExecutionEngine& engine, std::unordered_map<ExecutionNode*, ExecutionBlock*> const&) const {
ExecutionNode const* previousNode = getFirstDependency();
TRI_ASSERT(previousNode != nullptr);
auto inputRegisters = std::make_shared<std::unordered_set<RegisterId>>();
if (usesStartInVariable()) {
inputRegisters->emplace(varToRegUnchecked(startInVariable()));
}
if (usesTargetInVariable()) {
inputRegisters->emplace(varToRegUnchecked(targetInVariable()));
}
auto outputRegisters = std::make_shared<std::unordered_set<RegisterId>>();
TRI_ASSERT(usesPathOutVariable()); // This node always produces the path!
outputRegisters->emplace(varToRegUnchecked(pathOutVariable()));
auto opts = static_cast<ShortestPathOptions*>(options());
KShortestPathsExecutorInfos::InputVertex sourceInput = ::prepareVertexInput(this, false);
KShortestPathsExecutorInfos::InputVertex targetInput = ::prepareVertexInput(this, true);
std::unique_ptr<KShortestPathsFinder> finder;
finder.reset(new graph::KShortestPathsFinder(*opts));
TRI_ASSERT(finder != nullptr);
KShortestPathsExecutorInfos infos(inputRegisters, outputRegisters,
getRegisterPlan()->nrRegs[previousNode->getDepth()],
getRegisterPlan()->nrRegs[getDepth()],
getRegsToClear(), calcRegsToKeep(), std::move(finder),
std::move(sourceInput), std::move(targetInput));
return std::make_unique<ExecutionBlockImpl<KShortestPathsExecutor>>(&engine, this,
std::move(infos));
}
ExecutionNode* KShortestPathsNode::clone(ExecutionPlan* plan, bool withDependencies,
bool withProperties) const {
TRI_ASSERT(!_optionsBuilt);
auto oldOpts = static_cast<ShortestPathOptions*>(options());
std::unique_ptr<BaseOptions> tmp = std::make_unique<ShortestPathOptions>(*oldOpts);
auto c = std::make_unique<KShortestPathsNode>(plan, _id, _vocbase, _edgeColls,
_vertexColls, _directions, _inStartVariable,
_startVertexId, _inTargetVariable,
_targetVertexId, std::move(tmp));
if (usesPathOutVariable()) {
auto pathOutVariable = _pathOutVariable;
if (withProperties) {
pathOutVariable = plan->getAst()->variables()->createVariable(pathOutVariable);
}
TRI_ASSERT(pathOutVariable != nullptr);
c->setPathOutput(pathOutVariable);
}
// Temporary Filter Objects
c->_tmpObjVariable = _tmpObjVariable;
c->_tmpObjVarNode = _tmpObjVarNode;
c->_tmpIdNode = _tmpIdNode;
// Filter Condition Parts
c->_fromCondition = _fromCondition->clone(_plan->getAst());
c->_toCondition = _toCondition->clone(_plan->getAst());
return cloneHelper(std::move(c), withDependencies, withProperties);
}
void KShortestPathsNode::prepareOptions() {
if (_optionsBuilt) {
return;
}
TRI_ASSERT(!_optionsBuilt);
size_t numEdgeColls = _edgeColls.size();
Ast* ast = _plan->getAst();
auto opts = static_cast<ShortestPathOptions*>(options());
opts->setVariable(getTemporaryVariable());
// Compute Indexes.
for (size_t i = 0; i < numEdgeColls; ++i) {
auto dir = _directions[i];
switch (dir) {
case TRI_EDGE_IN:
opts->addLookupInfo(_plan, _edgeColls[i]->name(),
StaticStrings::ToString, _toCondition->clone(ast));
opts->addReverseLookupInfo(_plan, _edgeColls[i]->name(), StaticStrings::FromString,
_fromCondition->clone(ast));
break;
case TRI_EDGE_OUT:
opts->addLookupInfo(_plan, _edgeColls[i]->name(),
StaticStrings::FromString, _fromCondition->clone(ast));
opts->addReverseLookupInfo(_plan, _edgeColls[i]->name(), StaticStrings::ToString,
_toCondition->clone(ast));
break;
case TRI_EDGE_ANY:
TRI_ASSERT(false);
break;
}
}
// If we use the path output the cache should activate document
// caching otherwise it is not worth it.
if (ServerState::instance()->isCoordinator()) {
_options->activateCache(false, engines());
} else {
_options->activateCache(false, nullptr);
}
_optionsBuilt = true;
}

View File

@ -0,0 +1,163 @@
////////////////////////////////////////////////////////////////////////////////
/// DISCLAIMER
///
/// Copyright 2014-2016 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 Michael Hackstein
/// @author Markus Pfeiffer
////////////////////////////////////////////////////////////////////////////////
#ifndef ARANGOD_AQL_KSHORTEST_PATHS_NODE_H
#define ARANGOD_AQL_KSHORTEST_PATHS_NODE_H 1
#include "Aql/GraphNode.h"
#include "Aql/Graphs.h"
namespace arangodb {
namespace velocypack {
class Builder;
}
namespace graph {
struct BaseOptions;
struct ShortestPathOptions;
} // namespace graph
namespace aql {
/// @brief class KShortestPathsNode
class KShortestPathsNode : public GraphNode {
friend class ExecutionBlock;
friend class RedundantCalculationsReplacer;
/// @brief constructor with a vocbase and a collection name
public:
KShortestPathsNode(ExecutionPlan* plan, size_t id, TRI_vocbase_t* vocbase,
AstNode const* direction, AstNode const* start,
AstNode const* target, AstNode const* graph,
std::unique_ptr<graph::BaseOptions> options);
KShortestPathsNode(ExecutionPlan* plan, arangodb::velocypack::Slice const& base);
~KShortestPathsNode();
/// @brief Internal constructor to clone the node.
KShortestPathsNode(ExecutionPlan* plan, size_t id, TRI_vocbase_t* vocbase,
std::vector<std::unique_ptr<Collection>> const& edgeColls,
std::vector<std::unique_ptr<Collection>> const& vertexColls,
std::vector<TRI_edge_direction_e> const& directions,
Variable const* inStartVariable, std::string const& startVertexId,
Variable const* inTargetVariable, std::string const& targetVertexId,
std::unique_ptr<graph::BaseOptions> options);
public:
/// @brief return the type of the node
NodeType getType() const override final { return K_SHORTEST_PATHS; }
/// @brief export to VelocyPack
void toVelocyPackHelper(arangodb::velocypack::Builder&, unsigned flags) const override final;
/// @brief creates corresponding ExecutionBlock
std::unique_ptr<ExecutionBlock> createBlock(
ExecutionEngine& engine,
std::unordered_map<ExecutionNode*, ExecutionBlock*> const&) const override;
/// @brief clone ExecutionNode recursively
ExecutionNode* clone(ExecutionPlan* plan, bool withDependencies,
bool withProperties) const override final;
bool usesPathOutVariable() const { return _pathOutVariable != nullptr; }
Variable const& pathOutVariable() const {
TRI_ASSERT(_pathOutVariable != nullptr);
return *_pathOutVariable;
}
void setPathOutput(Variable const* outVar) { _pathOutVariable = outVar; }
/// @brief Test if this node uses an in variable or constant for start
bool usesStartInVariable() const { return _inStartVariable != nullptr; }
/// @brief return the start variable
Variable const& startInVariable() const {
TRI_ASSERT(_inStartVariable != nullptr);
return *_inStartVariable;
}
std::string const getStartVertex() const { return _startVertexId; }
/// @brief Test if this node uses an in variable or constant for target
bool usesTargetInVariable() const { return _inTargetVariable != nullptr; }
/// @brief return the target variable
Variable const& targetInVariable() const {
TRI_ASSERT(_inTargetVariable != nullptr);
return *_inTargetVariable;
}
std::string const getTargetVertex() const { return _targetVertexId; }
/// @brief getVariablesSetHere
std::vector<Variable const*> getVariablesSetHere() const override final {
std::vector<Variable const*> vars;
TRI_ASSERT(_pathOutVariable != nullptr);
vars.emplace_back(_pathOutVariable);
return vars;
}
/// @brief getVariablesUsedHere, modifying the set in-place
void getVariablesUsedHere(arangodb::HashSet<Variable const*>& vars) const override {
if (_inStartVariable != nullptr) {
vars.emplace(_inStartVariable);
}
if (_inTargetVariable != nullptr) {
vars.emplace(_inTargetVariable);
}
}
/// @brief Compute the shortest path options containing the expressions
/// MUST! be called after optimization and before creation
/// of blocks.
void prepareOptions() override;
private:
/// @brief path output variable
Variable const* _pathOutVariable;
/// @brief input variable only used if _vertexId is unused
Variable const* _inStartVariable;
/// @brief input vertexId only used if _inVariable is unused
std::string _startVertexId;
/// @brief input variable only used if _vertexId is unused
Variable const* _inTargetVariable;
/// @brief input vertexId only used if _inVariable is unused
std::string _targetVertexId;
/// @brief The hard coded condition on _from
AstNode* _fromCondition;
/// @brief The hard coded condition on _to
AstNode* _toCondition;
};
} // namespace aql
} // namespace arangodb
#endif

View File

@ -36,6 +36,7 @@
#include "Aql/Function.h"
#include "Aql/IResearchViewNode.h"
#include "Aql/IndexNode.h"
#include "Aql/KShortestPathsNode.h"
#include "Aql/ModificationNodes.h"
#include "Aql/Optimizer.h"
#include "Aql/Query.h"
@ -82,7 +83,9 @@ bool accessesCollectionVariable(arangodb::aql::ExecutionPlan const* plan,
}
if (setter->getType() == EN::INDEX || setter->getType() == EN::ENUMERATE_COLLECTION ||
setter->getType() == EN::ENUMERATE_IRESEARCH_VIEW ||
setter->getType() == EN::SUBQUERY || setter->getType() == EN::TRAVERSAL ||
setter->getType() == EN::SUBQUERY ||
setter->getType() == EN::TRAVERSAL ||
setter->getType() == EN::K_SHORTEST_PATHS ||
setter->getType() == EN::SHORTEST_PATH) {
return true;
}
@ -107,8 +110,10 @@ arangodb::aql::Collection const* getCollection(arangodb::aql::ExecutionNode cons
case EN::INDEX:
return ExecutionNode::castTo<arangodb::aql::IndexNode const*>(node)->collection();
case EN::TRAVERSAL:
case EN::K_SHORTEST_PATHS:
case EN::SHORTEST_PATH:
return ExecutionNode::castTo<arangodb::aql::GraphNode const*>(node)->collection();
default:
// note: modification nodes are not covered here yet
THROW_ARANGO_EXCEPTION_MESSAGE(TRI_ERROR_INTERNAL,
@ -345,6 +350,7 @@ class RestrictToSingleShardChecker final
switch (en->getType()) {
case EN::TRAVERSAL:
case EN::K_SHORTEST_PATHS:
case EN::SHORTEST_PATH: {
_stop = true;
return true; // abort enumerating, we are done already!
@ -1048,6 +1054,7 @@ void arangodb::aql::removeRedundantSortsRule(Optimizer* opt,
} else if (current->getType() == EN::ENUMERATE_LIST ||
current->getType() == EN::ENUMERATE_COLLECTION ||
current->getType() == EN::TRAVERSAL ||
current->getType() == EN::K_SHORTEST_PATHS ||
current->getType() == EN::SHORTEST_PATH) {
// ok, but we cannot remove two different sorts if one of these node
// types is between them
@ -1664,7 +1671,9 @@ void arangodb::aql::moveCalculationsDownRule(Optimizer* opt,
} else if (currentType == EN::INDEX || currentType == EN::ENUMERATE_COLLECTION ||
currentType == EN::ENUMERATE_IRESEARCH_VIEW ||
currentType == EN::ENUMERATE_LIST ||
currentType == EN::TRAVERSAL || currentType == EN::SHORTEST_PATH ||
currentType == EN::TRAVERSAL ||
currentType == EN::SHORTEST_PATH ||
currentType == EN::K_SHORTEST_PATHS ||
currentType == EN::COLLECT || currentType == EN::NORESULTS) {
// we will not push further down than such nodes
done = true;
@ -2084,6 +2093,11 @@ class arangodb::aql::RedundantCalculationsReplacer final
break;
}
case EN::K_SHORTEST_PATHS: {
replaceStartTargetVariables<ShortestPathNode>(en);
break;
}
case EN::SHORTEST_PATH: {
replaceStartTargetVariables<ShortestPathNode>(en);
break;
@ -3118,6 +3132,7 @@ struct SortToIndexNode final : public WalkerWorker<ExecutionNode> {
bool before(ExecutionNode* en) override final {
switch (en->getType()) {
case EN::TRAVERSAL:
case EN::K_SHORTEST_PATHS:
case EN::SHORTEST_PATH:
case EN::ENUMERATE_LIST:
case EN::ENUMERATE_IRESEARCH_VIEW:
@ -3507,6 +3522,7 @@ void arangodb::aql::optimizeClusterSingleShardRule(Optimizer* opt,
SmallVector<ExecutionNode*>::allocator_type::arena_type s;
SmallVector<ExecutionNode*> nodes{s};
std::vector<ExecutionNode::NodeType> types = {ExecutionNode::TRAVERSAL,
ExecutionNode::K_SHORTEST_PATHS,
ExecutionNode::SHORTEST_PATH,
ExecutionNode::SUBQUERY};
plan->findNodesOfType(nodes, types, true);
@ -4336,6 +4352,7 @@ void arangodb::aql::distributeFilternCalcToClusterRule(Optimizer* opt,
case EN::INDEX:
case EN::ENUMERATE_COLLECTION:
case EN::TRAVERSAL:
case EN::K_SHORTEST_PATHS:
case EN::SHORTEST_PATH:
case EN::SUBQUERY:
case EN::ENUMERATE_IRESEARCH_VIEW:
@ -4470,6 +4487,7 @@ void arangodb::aql::distributeSortToClusterRule(Optimizer* opt,
case EN::LIMIT:
case EN::INDEX:
case EN::TRAVERSAL:
case EN::K_SHORTEST_PATHS:
case EN::SHORTEST_PATH:
case EN::REMOTESINGLE:
case EN::ENUMERATE_IRESEARCH_VIEW:
@ -4999,6 +5017,7 @@ class RemoveToEnumCollFinder final : public WalkerWorker<ExecutionNode> {
case EN::LIMIT:
case EN::SORT:
case EN::TRAVERSAL:
case EN::K_SHORTEST_PATHS:
case EN::SHORTEST_PATH: {
// if we meet any of the above, then we abort . . .
break;
@ -5683,7 +5702,7 @@ void arangodb::aql::patchUpdateStatementsRule(Optimizer* opt,
}
modified = true;
}
} else if (type == EN::TRAVERSAL || type == EN::SHORTEST_PATH) {
} else if (type == EN::TRAVERSAL || type == EN::K_SHORTEST_PATHS || type == EN::SHORTEST_PATH) {
// unclear what will be read by the traversal
modified = false;
break;
@ -5907,6 +5926,7 @@ void arangodb::aql::prepareTraversalsRule(Optimizer* opt,
SmallVector<ExecutionNode*>::allocator_type::arena_type a;
SmallVector<ExecutionNode*> tNodes{a};
plan->findNodesOfType(tNodes, EN::TRAVERSAL, true);
plan->findNodesOfType(tNodes, EN::K_SHORTEST_PATHS, true);
plan->findNodesOfType(tNodes, EN::SHORTEST_PATH, true);
if (tNodes.empty()) {
@ -5921,6 +5941,10 @@ void arangodb::aql::prepareTraversalsRule(Optimizer* opt,
if (n->getType() == EN::TRAVERSAL) {
TraversalNode* traversal = ExecutionNode::castTo<TraversalNode*>(n);
traversal->prepareOptions();
} else if(n->getType() == EN::K_SHORTEST_PATHS) {
TRI_ASSERT(n->getType() == EN::K_SHORTEST_PATHS);
KShortestPathsNode* spn = ExecutionNode::castTo<KShortestPathsNode*>(n);
spn->prepareOptions();
} else {
TRI_ASSERT(n->getType() == EN::SHORTEST_PATH);
ShortestPathNode* spn = ExecutionNode::castTo<ShortestPathNode*>(n);
@ -6806,7 +6830,9 @@ void arangodb::aql::geoIndexRule(Optimizer* opt, std::unique_ptr<ExecutionPlan>
current->getType() == EN::ENUMERATE_COLLECTION ||
current->getType() == EN::ENUMERATE_LIST ||
current->getType() == EN::ENUMERATE_IRESEARCH_VIEW ||
current->getType() == EN::TRAVERSAL || current->getType() == EN::SHORTEST_PATH) {
current->getType() == EN::TRAVERSAL ||
current->getType() == EN::K_SHORTEST_PATHS ||
current->getType() == EN::SHORTEST_PATH) {
// invalidate limit and sort. filters can still be used
limit = nullptr;
info.sorted = false;
@ -6846,7 +6872,9 @@ void arangodb::aql::sortLimitRule(Optimizer* opt, std::unique_ptr<ExecutionPlan>
current->getType() == EN::ENUMERATE_COLLECTION ||
current->getType() == EN::ENUMERATE_LIST ||
current->getType() == EN::ENUMERATE_IRESEARCH_VIEW ||
current->getType() == EN::TRAVERSAL || current->getType() == EN::SHORTEST_PATH ||
current->getType() == EN::TRAVERSAL ||
current->getType() == EN::SHORTEST_PATH ||
current->getType() == EN::K_SHORTEST_PATHS ||
current->getType() == EN::INDEX || current->getType() == EN::COLLECT) {
// TODO check other end conditions
break; // stop parsing

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,9 @@
/* A Bison parser, made by GNU Bison 3.0.4. */
/* A Bison parser, made by GNU Bison 3.3.2. */
/* Bison interface for Yacc-like parsers in C
Copyright (C) 1984, 1989-1990, 2000-2015 Free Software Foundation, Inc.
Copyright (C) 1984, 1989-1990, 2000-2015, 2018-2019 Free Software Foundation,
Inc.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@ -30,6 +31,9 @@
This special exception was added by the Free Software Foundation in
version 2.2 of Bison. */
/* Undocumented macros, especially those whose name start with YY_,
are private implementation details. Do not rely on them. */
#ifndef YY_AQL_AQL_GRAMMAR_HPP_INCLUDED
# define YY_AQL_AQL_GRAMMAR_HPP_INCLUDED
/* Debug traces. */
@ -61,62 +65,63 @@ extern int Aqldebug;
T_AGGREGATE = 270,
T_GRAPH = 271,
T_SHORTEST_PATH = 272,
T_DISTINCT = 273,
T_REMOVE = 274,
T_INSERT = 275,
T_UPDATE = 276,
T_REPLACE = 277,
T_UPSERT = 278,
T_NULL = 279,
T_TRUE = 280,
T_FALSE = 281,
T_STRING = 282,
T_QUOTED_STRING = 283,
T_INTEGER = 284,
T_DOUBLE = 285,
T_PARAMETER = 286,
T_DATA_SOURCE_PARAMETER = 287,
T_ASSIGN = 288,
T_NOT = 289,
T_AND = 290,
T_OR = 291,
T_NIN = 292,
T_REGEX_MATCH = 293,
T_REGEX_NON_MATCH = 294,
T_EQ = 295,
T_NE = 296,
T_LT = 297,
T_GT = 298,
T_LE = 299,
T_GE = 300,
T_LIKE = 301,
T_PLUS = 302,
T_MINUS = 303,
T_TIMES = 304,
T_DIV = 305,
T_MOD = 306,
T_QUESTION = 307,
T_COLON = 308,
T_SCOPE = 309,
T_RANGE = 310,
T_COMMA = 311,
T_OPEN = 312,
T_CLOSE = 313,
T_OBJECT_OPEN = 314,
T_OBJECT_CLOSE = 315,
T_ARRAY_OPEN = 316,
T_ARRAY_CLOSE = 317,
T_OUTBOUND = 318,
T_INBOUND = 319,
T_ANY = 320,
T_ALL = 321,
T_NONE = 322,
UMINUS = 323,
UPLUS = 324,
FUNCCALL = 325,
REFERENCE = 326,
INDEXED = 327,
EXPANSION = 328
T_K_SHORTEST_PATHS = 273,
T_DISTINCT = 274,
T_REMOVE = 275,
T_INSERT = 276,
T_UPDATE = 277,
T_REPLACE = 278,
T_UPSERT = 279,
T_NULL = 280,
T_TRUE = 281,
T_FALSE = 282,
T_STRING = 283,
T_QUOTED_STRING = 284,
T_INTEGER = 285,
T_DOUBLE = 286,
T_PARAMETER = 287,
T_DATA_SOURCE_PARAMETER = 288,
T_ASSIGN = 289,
T_NOT = 290,
T_AND = 291,
T_OR = 292,
T_NIN = 293,
T_REGEX_MATCH = 294,
T_REGEX_NON_MATCH = 295,
T_EQ = 296,
T_NE = 297,
T_LT = 298,
T_GT = 299,
T_LE = 300,
T_GE = 301,
T_LIKE = 302,
T_PLUS = 303,
T_MINUS = 304,
T_TIMES = 305,
T_DIV = 306,
T_MOD = 307,
T_QUESTION = 308,
T_COLON = 309,
T_SCOPE = 310,
T_RANGE = 311,
T_COMMA = 312,
T_OPEN = 313,
T_CLOSE = 314,
T_OBJECT_OPEN = 315,
T_OBJECT_CLOSE = 316,
T_ARRAY_OPEN = 317,
T_ARRAY_CLOSE = 318,
T_OUTBOUND = 319,
T_INBOUND = 320,
T_ANY = 321,
T_ALL = 322,
T_NONE = 323,
UMINUS = 324,
UPLUS = 325,
FUNCCALL = 326,
REFERENCE = 327,
INDEXED = 328,
EXPANSION = 329
};
#endif
@ -125,7 +130,7 @@ extern int Aqldebug;
union YYSTYPE
{
#line 35 "Aql/grammar.y" /* yacc.c:1915 */
#line 35 "Aql/grammar.y" /* yacc.c:1921 */
arangodb::aql::AstNode* node;
struct {
@ -135,7 +140,7 @@ union YYSTYPE
bool boolval;
int64_t intval;
#line 139 "Aql/grammar.hpp" /* yacc.c:1915 */
#line 144 "Aql/grammar.hpp" /* yacc.c:1921 */
};
typedef union YYSTYPE YYSTYPE;

View File

@ -1,8 +1,9 @@
/* A Bison parser, made by GNU Bison 3.0.4. */
/* A Bison parser, made by GNU Bison 3.3.2. */
/* Bison interface for Yacc-like parsers in C
Copyright (C) 1984, 1989-1990, 2000-2015 Free Software Foundation, Inc.
Copyright (C) 1984, 1989-1990, 2000-2015, 2018-2019 Free Software Foundation,
Inc.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@ -30,6 +31,9 @@
This special exception was added by the Free Software Foundation in
version 2.2 of Bison. */
/* Undocumented macros, especially those whose name start with YY_,
are private implementation details. Do not rely on them. */
#ifndef YY_AQL_AQL_GRAMMAR_HPP_INCLUDED
# define YY_AQL_AQL_GRAMMAR_HPP_INCLUDED
/* Debug traces. */
@ -61,62 +65,63 @@ extern int Aqldebug;
T_AGGREGATE = 270,
T_GRAPH = 271,
T_SHORTEST_PATH = 272,
T_DISTINCT = 273,
T_REMOVE = 274,
T_INSERT = 275,
T_UPDATE = 276,
T_REPLACE = 277,
T_UPSERT = 278,
T_NULL = 279,
T_TRUE = 280,
T_FALSE = 281,
T_STRING = 282,
T_QUOTED_STRING = 283,
T_INTEGER = 284,
T_DOUBLE = 285,
T_PARAMETER = 286,
T_DATA_SOURCE_PARAMETER = 287,
T_ASSIGN = 288,
T_NOT = 289,
T_AND = 290,
T_OR = 291,
T_NIN = 292,
T_REGEX_MATCH = 293,
T_REGEX_NON_MATCH = 294,
T_EQ = 295,
T_NE = 296,
T_LT = 297,
T_GT = 298,
T_LE = 299,
T_GE = 300,
T_LIKE = 301,
T_PLUS = 302,
T_MINUS = 303,
T_TIMES = 304,
T_DIV = 305,
T_MOD = 306,
T_QUESTION = 307,
T_COLON = 308,
T_SCOPE = 309,
T_RANGE = 310,
T_COMMA = 311,
T_OPEN = 312,
T_CLOSE = 313,
T_OBJECT_OPEN = 314,
T_OBJECT_CLOSE = 315,
T_ARRAY_OPEN = 316,
T_ARRAY_CLOSE = 317,
T_OUTBOUND = 318,
T_INBOUND = 319,
T_ANY = 320,
T_ALL = 321,
T_NONE = 322,
UMINUS = 323,
UPLUS = 324,
FUNCCALL = 325,
REFERENCE = 326,
INDEXED = 327,
EXPANSION = 328
T_K_SHORTEST_PATHS = 273,
T_DISTINCT = 274,
T_REMOVE = 275,
T_INSERT = 276,
T_UPDATE = 277,
T_REPLACE = 278,
T_UPSERT = 279,
T_NULL = 280,
T_TRUE = 281,
T_FALSE = 282,
T_STRING = 283,
T_QUOTED_STRING = 284,
T_INTEGER = 285,
T_DOUBLE = 286,
T_PARAMETER = 287,
T_DATA_SOURCE_PARAMETER = 288,
T_ASSIGN = 289,
T_NOT = 290,
T_AND = 291,
T_OR = 292,
T_NIN = 293,
T_REGEX_MATCH = 294,
T_REGEX_NON_MATCH = 295,
T_EQ = 296,
T_NE = 297,
T_LT = 298,
T_GT = 299,
T_LE = 300,
T_GE = 301,
T_LIKE = 302,
T_PLUS = 303,
T_MINUS = 304,
T_TIMES = 305,
T_DIV = 306,
T_MOD = 307,
T_QUESTION = 308,
T_COLON = 309,
T_SCOPE = 310,
T_RANGE = 311,
T_COMMA = 312,
T_OPEN = 313,
T_CLOSE = 314,
T_OBJECT_OPEN = 315,
T_OBJECT_CLOSE = 316,
T_ARRAY_OPEN = 317,
T_ARRAY_CLOSE = 318,
T_OUTBOUND = 319,
T_INBOUND = 320,
T_ANY = 321,
T_ALL = 322,
T_NONE = 323,
UMINUS = 324,
UPLUS = 325,
FUNCCALL = 326,
REFERENCE = 327,
INDEXED = 328,
EXPANSION = 329
};
#endif
@ -125,7 +130,7 @@ extern int Aqldebug;
union YYSTYPE
{
#line 35 "Aql/grammar.y" /* yacc.c:1915 */
#line 35 "Aql/grammar.y" /* yacc.c:1921 */
arangodb::aql::AstNode* node;
struct {
@ -135,7 +140,7 @@ union YYSTYPE
bool boolval;
int64_t intval;
#line 139 "Aql/grammar.hpp" /* yacc.c:1915 */
#line 144 "Aql/grammar.hpp" /* yacc.c:1921 */
};
typedef union YYSTYPE YYSTYPE;

View File

@ -231,6 +231,7 @@ static AstNode* TransformOutputVariables(Parser* parser, AstNode const* names) {
%token T_GRAPH "GRAPH keyword"
%token T_SHORTEST_PATH "SHORTEST_PATH keyword"
%token T_K_SHORTEST_PATHS "K_SHORTEST_PATHS keyword"
%token T_DISTINCT "DISTINCT modifier"
%token T_REMOVE "REMOVE command"
@ -352,6 +353,7 @@ static AstNode* TransformOutputVariables(Parser* parser, AstNode const* names) {
%type <node> for_output_variables;
%type <node> traversal_graph_info;
%type <node> shortest_path_graph_info;
%type <node> k_shortest_paths_graph_info;
%type <node> optional_array_elements;
%type <node> array_elements_list;
%type <node> for_options;
@ -607,7 +609,28 @@ shortest_path_graph_info:
}
;
k_shortest_paths_graph_info:
graph_direction T_K_SHORTEST_PATHS expression T_STRING expression graph_subject options {
if (!TRI_CaseEqualString($4.value, "TO")) {
parser->registerParseError(TRI_ERROR_QUERY_PARSE, "unexpected qualifier '%s', expecting 'TO'", $4.value, yylloc.first_line, yylloc.first_column);
}
auto infoNode = parser->ast()->createNodeArray();
auto dirNode = parser->ast()->createNodeValueInt($1);
// Direction
infoNode->addMember(dirNode);
// Source
infoNode->addMember($3);
// Target
infoNode->addMember($5);
// Graph
infoNode->addMember($6);
// Opts
auto opts = parser->ast()->createNodeOptions($7);
TRI_ASSERT(opts != nullptr);
infoNode->addMember(opts);
$$ = infoNode;
}
;
for_statement:
T_FOR for_output_variables T_IN expression {
@ -709,6 +732,25 @@ for_statement:
parser->ast()->addOperation(node);
}
| T_FOR for_output_variables T_IN k_shortest_paths_graph_info {
// first open a new scope (after expression is evaluated)
parser->ast()->scopes()->start(arangodb::aql::AQL_SCOPE_FOR);
// K Shortest Paths
auto variableNamesNode = static_cast<AstNode*>($2);
TRI_ASSERT(variableNamesNode != nullptr);
TRI_ASSERT(variableNamesNode->type == NODE_TYPE_ARRAY);
if (variableNamesNode->numMembers() > 2) {
parser->registerParseError(TRI_ERROR_QUERY_PARSE, "ShortestPath only has one or two return variables", yylloc.first_line, yylloc.first_column);
}
auto variablesNode = TransformOutputVariables(parser, variableNamesNode);
auto graphInfoNode = static_cast<AstNode*>($4);
TRI_ASSERT(graphInfoNode != nullptr);
TRI_ASSERT(graphInfoNode->type == NODE_TYPE_ARRAY);
auto node = parser->ast()->createNodeKShortestPaths(variablesNode, graphInfoNode);
parser->ast()->addOperation(node);
}
;
filter_statement:

File diff suppressed because it is too large Load Diff

View File

@ -162,6 +162,10 @@ class Parser;
return T_SHORTEST_PATH;
}
(?i:K_SHORTEST_PATHS) {
return T_K_SHORTEST_PATHS;
}
(?i:OUTBOUND) {
return T_OUTBOUND;
}

View File

@ -229,6 +229,8 @@ SET(ARANGOD_SOURCES
Aql/IndexHint.cpp
Aql/IndexNode.cpp
Aql/InputAqlItemRow.cpp
Aql/KShortestPathsExecutor.cpp
Aql/KShortestPathsNode.cpp
Aql/LimitExecutor.cpp
Aql/ModificationExecutor.cpp
Aql/ModificationExecutorTraits.cpp
@ -378,6 +380,7 @@ SET(ARANGOD_SOURCES
Graph/Graph.cpp
Graph/GraphManager.cpp
Graph/GraphOperations.cpp
Graph/KShortestPathsFinder.cpp
Graph/NeighborsEnumerator.cpp
Graph/PathEnumerator.cpp
Graph/ShortestPathFinder.cpp

View File

@ -74,6 +74,14 @@ struct EdgeDocumentToken {
#endif
}
EdgeDocumentToken& operator=(EdgeDocumentToken const& edtkn) {
#ifdef ARANGODB_ENABLE_MAINTAINER_MODE
_type = edtkn._type;
#endif
_data = edtkn._data;
return *this;
}
EdgeDocumentToken& operator=(EdgeDocumentToken&& edtkn) {
#ifdef ARANGODB_ENABLE_MAINTAINER_MODE
_type = edtkn._type;

View File

@ -0,0 +1,381 @@
////////////////////////////////////////////////////////////////////////////////
/// DISCLAIMER
///
/// Copyright 2014-2016 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 Markus Pfeiffer
////////////////////////////////////////////////////////////////////////////////
#include "KShortestPathsFinder.h"
#include "Aql/AqlValue.h"
#include "Cluster/ServerState.h"
#include "Graph/EdgeCursor.h"
#include "Graph/EdgeDocumentToken.h"
#include "Graph/ShortestPathOptions.h"
#include "Graph/ShortestPathResult.h"
#include "Graph/TraverserCache.h"
#include "Transaction/Helpers.h"
#include "Utils/OperationCursor.h"
#include "VocBase/LogicalCollection.h"
#include <velocypack/Iterator.h>
#include <velocypack/Slice.h>
#include <velocypack/StringRef.h>
#include <velocypack/velocypack-aliases.h>
using namespace arangodb;
using namespace arangodb::graph;
//
KShortestPathsFinder::KShortestPathsFinder(ShortestPathOptions& options)
: ShortestPathFinder(options), _pathAvailable(false) {}
KShortestPathsFinder::~KShortestPathsFinder() {}
// Sets up k-shortest-paths traversal from start to end
// Returns number of currently known paths
bool KShortestPathsFinder::startKShortestPathsTraversal(
arangodb::velocypack::Slice const& start, arangodb::velocypack::Slice const& end) {
_start = arangodb::velocypack::StringRef(start);
_end = arangodb::velocypack::StringRef(end);
_pathAvailable = true;
_shortestPaths.clear();
_candidatePaths.clear();
TRI_IF_FAILURE("TraversalOOMInitialize") {
THROW_ARANGO_EXCEPTION(TRI_ERROR_DEBUG);
}
return true;
}
bool KShortestPathsFinder::computeShortestPath(
VertexRef const& start, VertexRef const& end,
std::unordered_set<VertexRef> const& forbiddenVertices,
std::unordered_set<Edge> const& forbiddenEdges, Path& result) {
bool found = false;
Ball left(start, FORWARD);
Ball right(end, BACKWARD);
VertexRef join;
result.clear();
while (!left._frontier.empty() && !right._frontier.empty() && !found) {
_options.isQueryKilledCallback();
// Choose the smaller frontier to expand.
if (left._frontier.size() < right._frontier.size()) {
found = advanceFrontier(left, right, forbiddenVertices, forbiddenEdges, join);
} else {
found = advanceFrontier(right, left, forbiddenVertices, forbiddenEdges, join);
}
}
if (found) {
reconstructPath(left, right, join, result);
}
return found;
}
void KShortestPathsFinder::computeNeighbourhoodOfVertex(
VertexRef vertex, Direction direction, std::vector<Step>& steps) {
std::unique_ptr<EdgeCursor> edgeCursor;
switch (direction) {
case BACKWARD:
edgeCursor.reset(_options.nextReverseCursor(vertex));
break;
case FORWARD:
edgeCursor.reset(_options.nextCursor(vertex));
break;
default:
TRI_ASSERT(false);
}
// TODO: This is a bit of a hack
if (_options.useWeight()) {
auto callback = [&](EdgeDocumentToken&& eid, VPackSlice edge, size_t cursorIdx) -> void {
if (edge.isString()) {
VPackSlice doc = _options.cache()->lookupToken(eid);
double weight = _options.weightEdge(doc);
if (edge.compareString(vertex.data(), vertex.length()) != 0) {
VertexRef id = _options.cache()->persistString(VertexRef(edge));
steps.emplace_back(std::move(eid), id, weight);
}
} else {
VertexRef other(transaction::helpers::extractFromFromDocument(edge));
if (other == vertex) {
other = VertexRef(transaction::helpers::extractToFromDocument(edge));
}
if (other != vertex) {
VertexRef id = _options.cache()->persistString(other);
steps.emplace_back(std::move(eid), id, _options.weightEdge(edge));
}
}
};
edgeCursor->readAll(callback);
} else {
auto callback = [&](EdgeDocumentToken&& eid, VPackSlice edge, size_t cursorIdx) -> void {
if (edge.isString()) {
if (edge.compareString(vertex.data(), vertex.length()) != 0) {
VertexRef id = _options.cache()->persistString(VertexRef(edge));
steps.emplace_back(std::move(eid), id, 1);
}
} else {
VertexRef other(transaction::helpers::extractFromFromDocument(edge));
if (other == vertex) {
other = VertexRef(transaction::helpers::extractToFromDocument(edge));
}
if (other != vertex) {
VertexRef id = _options.cache()->persistString(other);
steps.emplace_back(std::move(eid), id, 1);
}
}
};
edgeCursor->readAll(callback);
}
}
bool KShortestPathsFinder::advanceFrontier(
Ball& source, Ball const& target, std::unordered_set<VertexRef> const& forbiddenVertices,
std::unordered_set<Edge> const& forbiddenEdges, VertexRef& join) {
std::vector<Step> neighbours;
VertexRef vr;
FoundVertex* v;
bool success = source._frontier.popMinimal(vr, v, true);
if (!success) {
return false;
}
neighbours.clear();
computeNeighbourhoodOfVertex(vr, source._direction, neighbours);
for (auto& s : neighbours) {
if (forbiddenEdges.find(s._edge) == forbiddenEdges.end() &&
forbiddenVertices.find(s._vertex) == forbiddenVertices.end()) {
double weight = v->weight() + s._weight;
auto lookup = source._frontier.find(s._vertex);
if (lookup != nullptr) {
if (lookup->weight() > weight) {
source._frontier.lowerWeight(s._vertex, weight);
lookup->_pred = vr;
lookup->_edge = s._edge;
}
} else {
source._frontier.insert(s._vertex,
new FoundVertex(s._vertex, vr, std::move(s._edge), weight));
auto found = target._frontier.find(s._vertex);
if (found != nullptr) {
join = s._vertex;
return true;
}
}
}
}
return false;
}
void KShortestPathsFinder::reconstructPath(Ball const& left, Ball const& right,
VertexRef const& join,
Path& result) {
result.clear();
TRI_ASSERT(!join.empty());
result._vertices.emplace_back(join);
FoundVertex* it;
it = left._frontier.find(join);
TRI_ASSERT(it != nullptr);
double startToJoin = it->weight();
result._weight = startToJoin;
while (it != nullptr && it->_weight > 0) {
result._vertices.push_front(it->_pred);
result._edges.push_front(it->_edge);
result._weights.push_front(it->_weight);
it = left._frontier.find(it->_pred);
}
// Initial vertex has weight 0
result._weights.push_front(0);
it = right._frontier.find(join);
TRI_ASSERT(it != nullptr);
double joinToEnd = it->_weight;
result._weight += joinToEnd;
while (it != nullptr && it->_weight > 0) {
result._vertices.emplace_back(it->_pred);
result._edges.emplace_back(it->_edge);
it = right._frontier.find(it->_pred);
TRI_ASSERT(it != nullptr); // should run into 0 weight before
result._weights.emplace_back(startToJoin + (joinToEnd - it->_weight));
}
TRI_IF_FAILURE("TraversalOOMPath") {
THROW_ARANGO_EXCEPTION(TRI_ERROR_DEBUG);
}
}
bool KShortestPathsFinder::computeNextShortestPath(Path& result) {
std::unordered_set<VertexRef> forbiddenVertices;
std::unordered_set<Edge> forbiddenEdges;
Path tmpPath, candidate;
TRI_ASSERT(!_shortestPaths.empty());
auto& lastShortestPath = _shortestPaths.back();
bool available = false;
for (size_t i = 0; i < lastShortestPath.length() - 1; ++i) {
auto& spur = lastShortestPath._vertices.at(i);
forbiddenVertices.clear();
forbiddenEdges.clear();
// Must not use vertices on the prefix
for (size_t j = 0; j < i; ++j) {
forbiddenVertices.emplace(lastShortestPath._vertices[j]);
}
// TODO: This can be done more efficiently by storing shortest
// paths in a prefix/postfix tree
for (auto const& p : _shortestPaths) {
bool eq = true;
for (size_t e = 0; e < i; ++e) {
if (!p._edges.at(e).equals(lastShortestPath._edges.at(e))) {
eq = false;
break;
}
}
if (eq && (i < p._edges.size())) {
forbiddenEdges.emplace(p._edges.at(i));
}
}
if (computeShortestPath(spur, _end, forbiddenVertices, forbiddenEdges, tmpPath)) {
candidate.clear();
candidate.append(lastShortestPath, 0, i);
candidate.append(tmpPath, 0, tmpPath.length() - 1);
_candidatePaths.emplace_back(candidate);
}
}
if (!_candidatePaths.empty()) {
// TODO: hack, _candidatePaths should be a priority queue
// indeed one that removes duplicates automatically
// Sorted in reverse to have pop_back
if (_options.useWeight()) {
std::sort(_candidatePaths.begin(), _candidatePaths.end(), [](Path const& p1, Path const& p2) {
return p1._weight > p2._weight;
});
} else {
std::sort(_candidatePaths.begin(), _candidatePaths.end(), [](Path const& p1, Path const& p2) {
return p1._vertices.size() > p2._vertices.size();
});
}
// FIXME: this is of course bad.
_candidatePaths.erase(std::unique(_candidatePaths.begin(), _candidatePaths.end()),
_candidatePaths.end());
auto const& p = _candidatePaths.back();
result.clear();
result.append(p, 0, p.length() - 1);
_candidatePaths.pop_back();
available = true;
}
return available;
}
bool KShortestPathsFinder::getNextPath(Path& result) {
bool available = false;
result.clear();
// TODO: this looks a bit ugly
if (_shortestPaths.empty()) {
if (_start == _end) {
TRI_ASSERT(!_start.empty());
result._vertices.emplace_back(_start);
result._weight = 0;
available = true;
} else {
available = computeShortestPath(_start, _end, {}, {}, result);
}
} else {
if (_start == _end) {
available = false;
} else {
available = computeNextShortestPath(result);
}
}
if (available) {
_shortestPaths.emplace_back(result);
_options.fetchVerticesCoordinator(result._vertices);
TRI_IF_FAILURE("TraversalOOMPath") {
THROW_ARANGO_EXCEPTION(TRI_ERROR_DEBUG);
}
}
_pathAvailable = available;
return available;
}
bool KShortestPathsFinder::getNextPathShortestPathResult(ShortestPathResult& result) {
Path path;
result.clear();
if (getNextPath(path)) {
result._vertices = path._vertices;
result._edges = path._edges;
return true;
} else {
return false;
}
}
bool KShortestPathsFinder::getNextPathAql(arangodb::velocypack::Builder& result) {
Path path;
if (getNextPath(path)) {
result.clear();
result.openObject();
result.add(VPackValue("edges"));
result.openArray();
for (auto const& it : path._edges) {
_options.cache()->insertEdgeIntoResult(it, result);
}
result.close(); // Array
result.add(VPackValue("vertices"));
result.openArray();
for (auto const& it : path._vertices) {
_options.cache()->insertVertexIntoResult(it, result);
}
result.close(); // Array
if (_options.useWeight()) {
result.add("weight", VPackValue(path._weight));
} else {
// If not using weight, weight is defined as 1 per edge
result.add("weight", VPackValue(path._edges.size()));
}
result.close(); // Object
TRI_ASSERT(result.isClosed());
return true;
} else {
return false;
}
}

View File

@ -0,0 +1,218 @@
////////////////////////////////////////////////////////////////////////////////
/// 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 Markus Pfeiffer
////////////////////////////////////////////////////////////////////////////////
#ifndef ARANGODB_GRAPH_CONSTANT_WEIGHT_K_SHORTEST_PATHS_FINDER_H
#define ARANGODB_GRAPH_CONSTANT_WEIGHT_K_SHORTEST_PATHS_FINDER_H 1
#include "Aql/AqlValue.h"
#include "Basics/VelocyPackHelper.h"
#include "Graph/EdgeDocumentToken.h"
#include "Graph/ShortestPathFinder.h"
#include "Graph/ShortestPathPriorityQueue.h"
#include <velocypack/StringRef.h>
namespace arangodb {
namespace velocypack {
class Slice;
}
namespace graph {
struct ShortestPathOptions;
// Inherit from ShortestPathfinder to get destroyEngines and not copy it
// again.
// TODO: Traverser.h has destroyEngines as well (the code for the two functions
// is identical), refactor?
class KShortestPathsFinder : public ShortestPathFinder {
private:
// Mainly for readability
typedef arangodb::velocypack::StringRef VertexRef;
typedef arangodb::graph::EdgeDocumentToken Edge;
enum Direction { FORWARD, BACKWARD };
// TODO: This could be merged with ShortestPathResult
struct Path {
std::deque<VertexRef> _vertices;
std::deque<Edge> _edges;
// weight of path to vertex
// where _weights.front() == 0 and
// _weights.back() == _weight.
std::deque<double> _weights;
double _weight;
void clear() {
_vertices.clear();
_edges.clear();
_weights.clear();
_weight = 0;
};
size_t length() const { return _vertices.size(); };
void append(Path const& p, size_t a, size_t b) {
if (this->length() == 0) {
_vertices.emplace_back(p._vertices.at(a));
_weights.emplace_back(0);
}
// Only append paths where the first vertex of p
// is the same as the last vertex of this.
TRI_ASSERT((_vertices.back().equals(p._vertices.front())));
double ew = _weights.back();
double pw = p._weights.at(a);
while (a < b) {
_edges.emplace_back(p._edges.at(a));
a++;
_vertices.emplace_back(p._vertices.at(a));
_weights.emplace_back(ew + (p._weights.at(a) - pw));
}
_weight = _weights.back();
};
// TODO: implement == for EdgeDocumentToken and VertexRef
// so these things become less cluttery
bool operator==(Path const& rhs) const {
if (_edges.size() != rhs._edges.size() ||
_vertices.size() != rhs._vertices.size()) {
return false;
}
for (size_t i = 0; i < _vertices.size(); ++i) {
if (!_vertices.at(i).equals(rhs._vertices.at(i))) {
return false;
}
}
for (size_t i = 0; i < _edges.size(); ++i) {
if (!_edges.at(i).equals(rhs._edges.at(i))) {
return false;
}
}
return true;
};
};
struct Step {
Edge _edge;
VertexRef _vertex;
double _weight;
Step(Edge const& edge, VertexRef const& vertex, double weight)
: _edge(edge), _vertex(vertex), _weight(weight){};
};
struct FoundVertex {
VertexRef _vertex;
VertexRef _pred;
Edge _edge;
double _weight;
// Using negative weight to signifiy start/end vertex
FoundVertex(VertexRef const& vertex) : _vertex(vertex), _weight(0){};
FoundVertex(VertexRef const& vertex, VertexRef const& pred, Edge&& edge, double weight)
: _vertex(vertex), _pred(pred), _edge(std::move(edge)), _weight(weight){};
double weight() { return _weight; };
void setWeight(double weight) { _weight = weight; };
VertexRef const& getKey() { return _vertex; };
};
// typedef std::deque<VertexRef> Frontier;
typedef ShortestPathPriorityQueue<VertexRef, FoundVertex, double> Frontier;
// Contains the vertices that were found while searching
// for a shortest path between start and end together with
// the number of paths leading to that vertex and information
// how to trace paths from the vertex from start/to end.
typedef std::unordered_map<VertexRef, FoundVertex*> FoundVertices;
struct Ball {
VertexRef _centre;
Direction _direction;
Frontier _frontier;
Ball(void){};
Ball(VertexRef const& centre, Direction direction)
: _centre(centre), _direction(direction) {
auto v = new FoundVertex(centre);
_frontier.insert(centre, v);
};
~Ball() {
// TODO free all vertices
}
};
public:
explicit KShortestPathsFinder(ShortestPathOptions& options);
~KShortestPathsFinder();
// This is here because we inherit from ShortestPathFinder (to get the destroyEngines function)
// TODO: Remove
bool shortestPath(arangodb::velocypack::Slice const& start,
arangodb::velocypack::Slice const& target,
arangodb::graph::ShortestPathResult& result) {
TRI_ASSERT(false);
return false;
};
//
bool startKShortestPathsTraversal(arangodb::velocypack::Slice const& start,
arangodb::velocypack::Slice const& end);
// get the next available path as AQL value.
bool getNextPathAql(arangodb::velocypack::Builder& builder);
// get the next available path as a ShortestPathResult
// TODO: this is only here to not break catch-tests and needs a cleaner solution.
// probably by making ShortestPathResult versatile enough and using that
bool getNextPathShortestPathResult(ShortestPathResult& path);
// get the next available path as a Path
bool getNextPath(Path& path);
bool isPathAvailable(void) { return _pathAvailable; };
private:
bool computeShortestPath(VertexRef const& start, VertexRef const& end,
std::unordered_set<VertexRef> const& forbiddenVertices,
std::unordered_set<Edge> const& forbiddenEdges, Path& result);
bool computeNextShortestPath(Path& result);
void reconstructPath(Ball const& left, Ball const& right,
VertexRef const& join, Path& result);
void computeNeighbourhoodOfVertex(VertexRef vertex, Direction direction,
std::vector<Step>& steps);
// returns the number of paths found
// TODO: check why target can't be const
bool advanceFrontier(Ball& source, Ball const& target,
std::unordered_set<VertexRef> const& forbiddenVertices,
std::unordered_set<Edge> const& forbiddenEdges, VertexRef& join);
private:
bool _pathAvailable;
VertexRef _start;
VertexRef _end;
std::vector<Path> _shortestPaths;
// TODO: This should be a priority queue
std::vector<Path> _candidatePaths;
};
} // namespace graph
} // namespace arangodb
#endif

View File

@ -138,7 +138,7 @@ class ShortestPathPriorityQueue {
/// than via lowerWeight, otherwise the queue order could be violated.
//////////////////////////////////////////////////////////////////////////////
Value* find(Key const& k) {
Value* find(Key const& k) const {
auto it = _lookup.find(k);
if (it == _lookup.end()) {

View File

@ -46,12 +46,13 @@ namespace graph {
class AttributeWeightShortestPathFinder;
class ConstantWeightShortestPathFinder;
class KShortestPathsFinder;
class TraverserCache;
class ShortestPathResult {
friend class arangodb::graph::AttributeWeightShortestPathFinder;
friend class arangodb::graph::ConstantWeightShortestPathFinder;
friend class arangodb::graph::KShortestPathsFinder;
public:
//////////////////////////////////////////////////////////////////////////////
/// @brief Constructor. This is an abstract only class.

File diff suppressed because one or more lines are too long

View File

@ -3725,4 +3725,4 @@ var cutByResolution = function (str) {
</div>
</div></script><script id="warningList.ejs" type="text/template"> <% if (warnings.length > 0) { %> <div>
<ul> <% console.log(warnings); _.each(warnings, function(w) { console.log(w);%> <li><b><%=w.code%></b>: <%=w.message%></li> <% }); %> </ul>
</div> <% } %> </script></head><body><nav class="navbar" style="display: none"><div class="primary"><div class="navlogo"><a class="logo big" href="#"><img id="ArangoDBLogo" class="arangodbLogo" src="img/arangodb-edition-optimized.svg"></a><a class="logo small" href="#"><img class="arangodbLogo" src="img/arangodb_logo_small.png"></a><a class="version"><span id="currentVersion"></span></a></div><div class="statmenu" id="statisticBar"></div><div class="navmenu" id="navigationBar"></div></div></nav><div id="modalPlaceholder"></div><div class="bodyWrapper" style="display: none"><div class="centralRow"><div id="navbar2" class="navbarWrapper secondary"><div class="subnavmenu" id="subNavigationBar"></div></div><div class="resizecontainer contentWrapper"><div id="loadingScreen" class="loadingScreen" style="display: none"><i class="fa fa-circle-o-notch fa-spin fa-3x fa-fw margin-bottom"></i> <span class="sr-only">Loading...</span></div><div id="content" class="centralContent"></div><footer class="footer"><div id="footerBar"></div></footer></div></div></div><div id="progressPlaceholder" style="display:none"></div><div id="spotlightPlaceholder" style="display:none"></div><div id="graphSettingsContent" style="display: none"></div><div id="filterSelectDiv" style="display:none"></div><div id="offlinePlaceholder" style="display:none"><div class="offline-div"><div class="pure-u"><div class="pure-u-1-4"></div><div class="pure-u-1-2 offline-window"><div class="offline-header"><h3>You have been disconnected from the server</h3></div><div class="offline-body"><p>The connection to the server has been lost. The server may be under heavy load.</p><p>Trying to reconnect in <span id="offlineSeconds">10</span> seconds.</p><p class="animation_state"><span><button class="button-success">Reconnect now</button></span></p></div></div><div class="pure-u-1-4"></div></div></div></div><div class="arangoFrame" style=""><div class="outerDiv"><div class="innerDiv"></div></div></div><script src="libs.js?version=1553626406176"></script><script src="app.js?version=1553626406176"></script></body></html>
</div> <% } %> </script></head><body><nav class="navbar" style="display: none"><div class="primary"><div class="navlogo"><a class="logo big" href="#"><img id="ArangoDBLogo" class="arangodbLogo" src="img/arangodb-edition-optimized.svg"></a><a class="logo small" href="#"><img class="arangodbLogo" src="img/arangodb_logo_small.png"></a><a class="version"><span id="currentVersion"></span></a></div><div class="statmenu" id="statisticBar"></div><div class="navmenu" id="navigationBar"></div></div></nav><div id="modalPlaceholder"></div><div class="bodyWrapper" style="display: none"><div class="centralRow"><div id="navbar2" class="navbarWrapper secondary"><div class="subnavmenu" id="subNavigationBar"></div></div><div class="resizecontainer contentWrapper"><div id="loadingScreen" class="loadingScreen" style="display: none"><i class="fa fa-circle-o-notch fa-spin fa-3x fa-fw margin-bottom"></i> <span class="sr-only">Loading...</span></div><div id="content" class="centralContent"></div><footer class="footer"><div id="footerBar"></div></footer></div></div></div><div id="progressPlaceholder" style="display:none"></div><div id="spotlightPlaceholder" style="display:none"></div><div id="graphSettingsContent" style="display: none"></div><div id="filterSelectDiv" style="display:none"></div><div id="offlinePlaceholder" style="display:none"><div class="offline-div"><div class="pure-u"><div class="pure-u-1-4"></div><div class="pure-u-1-2 offline-window"><div class="offline-header"><h3>You have been disconnected from the server</h3></div><div class="offline-body"><p>The connection to the server has been lost. The server may be under heavy load.</p><p>Trying to reconnect in <span id="offlineSeconds">10</span> seconds.</p><p class="animation_state"><span><button class="button-success">Reconnect now</button></span></p></div></div><div class="pure-u-1-4"></div></div></div></div><div class="arangoFrame" style=""><div class="outerDiv"><div class="innerDiv"></div></div></div><script src="libs.js?version=1554822895711"></script><script src="app.js?version=1554822895711"></script></body></html>

View File

@ -28552,7 +28552,7 @@ var TextHighlightRules = require("./text_highlight_rules").TextHighlightRules;
var AqlHighlightRules = function() {
var keywords = (
"for|return|filter|search|sort|limit|let|collect|asc|desc|in|into|insert|update|remove|replace|upsert|options|with|and|or|not|distinct|graph|shortest_path|outbound|inbound|any|all|none|aggregate|like"
"for|return|filter|search|sort|limit|let|collect|asc|desc|in|into|insert|update|remove|replace|upsert|options|with|and|or|not|distinct|graph|shortest_path|outbound|inbound|any|all|none|aggregate|like|k_shortest_paths"
);
var builtinFunctions = (

View File

@ -88,7 +88,7 @@ var TextHighlightRules = require("./text_highlight_rules").TextHighlightRules;
var AqlHighlightRules = function() {
var keywords = (
"for|return|filter|search|sort|limit|let|collect|asc|desc|in|into|insert|update|remove|replace|upsert|options|with|and|or|not|distinct|graph|shortest_path|outbound|inbound|any|all|none|aggregate|like"
"for|return|filter|search|sort|limit|let|collect|asc|desc|in|into|insert|update|remove|replace|upsert|options|with|and|or|not|distinct|graph|shortest_path|outbound|inbound|any|all|none|aggregate|like|k_shortest_paths"
);
var builtinFunctions = (

View File

@ -1,47 +0,0 @@
{
"name": "aardvark",
"version": "2.0.0",
"description": "The ArangoDB web interface",
"author": "Heiko Kernbach, Michael Hackstein, moonglum",
"license": "Apache-2.0",
"scripts": {
"grunt": "grunt"
},
"devDependencies": {
"@babel/core": "^7.1.2",
"@babel/preset-env": "^7.1.0",
"babel-core": "^6.26.3",
"eslint": "^5.2.0",
"eslint-config-semistandard": "^12.0.1",
"eslint-config-standard": "^12.0.0",
"eslint-plugin-import": "^2.13.0",
"eslint-plugin-node": "^7.0.1",
"eslint-plugin-promise": "^4.0.1",
"eslint-plugin-standard": "^4.0.0",
"grunt": "^1.0.3",
"grunt-babel": "^8.0.0",
"grunt-concat-in-order": "^0.3.0",
"grunt-concurrent": "^2.3.1",
"grunt-contrib-compress": "^1.4.3",
"grunt-contrib-concat": "^1.0.1",
"grunt-contrib-cssmin": "^3.0.0",
"grunt-contrib-htmlmin": "^3.0.0",
"grunt-contrib-imagemin": "^2.0.1",
"grunt-contrib-jshint": "^2.0.0",
"grunt-contrib-sass": "^1.0.0",
"grunt-contrib-uglify": "^4.0.0",
"grunt-contrib-watch": "^1.1.0",
"grunt-eslint": "^21.0.0",
"grunt-processhtml": "^0.4.1",
"grunt-sass": "^3.0.2",
"grunt-text-replace": "^0.4.0",
"load-grunt-tasks": "^4.0.0",
"matchdep": "^2.0.0",
"node-sass": "^4.9.3",
"sass": "^1.10.1"
},
"repository": {
"type": "git",
"url": "git://github.com/arangodb/arangodb.git"
}
}

View File

@ -612,6 +612,69 @@ function printShortestPathDetails(shortestPaths) {
}
}
function printKShortestPathsDetails(shortestPaths) {
'use strict';
if (shortestPaths.length === 0) {
return;
}
stringBuilder.appendLine();
stringBuilder.appendLine(section('k shortest paths on graphs:'));
var maxIdLen = String('Id').length;
var maxVertexCollectionNameStrLen = String('Vertex collections').length;
var maxEdgeCollectionNameStrLen = String('Edge collections').length;
shortestPaths.forEach(function (node) {
var l = String(node.id).length;
if (l > maxIdLen) {
maxIdLen = l;
}
if (node.hasOwnProperty('vertexCollectionNameStr')) {
if (node.vertexCollectionNameStrLen > maxVertexCollectionNameStrLen) {
maxVertexCollectionNameStrLen = node.vertexCollectionNameStrLen;
}
}
if (node.hasOwnProperty('edgeCollectionNameStr')) {
if (node.edgeCollectionNameStrLen > maxEdgeCollectionNameStrLen) {
maxEdgeCollectionNameStrLen = node.edgeCollectionNameStrLen;
}
}
});
var line = ' ' + pad(1 + maxIdLen - String('Id').length) + header('Id') + ' ' +
header('Vertex collections') + pad(1 + maxVertexCollectionNameStrLen - 'Vertex collections'.length) + ' ' +
header('Edge collections') + pad(1 + maxEdgeCollectionNameStrLen - 'Edge collections'.length);
stringBuilder.appendLine(line);
for (let sp of shortestPaths) {
line = ' ' + pad(1 + maxIdLen - String(sp.id).length) + sp.id + ' ';
if (sp.hasOwnProperty('vertexCollectionNameStr')) {
line += sp.vertexCollectionNameStr +
pad(1 + maxVertexCollectionNameStrLen - sp.vertexCollectionNameStrLen) + ' ';
} else {
line += pad(1 + maxVertexCollectionNameStrLen) + ' ';
}
if (sp.hasOwnProperty('edgeCollectionNameStr')) {
line += sp.edgeCollectionNameStr +
pad(1 + maxEdgeCollectionNameStrLen - sp.edgeCollectionNameStrLen) + ' ';
} else {
line += pad(1 + maxEdgeCollectionNameStrLen) + ' ';
}
if (sp.hasOwnProperty('ConditionStr')) {
line += sp.ConditionStr;
}
stringBuilder.appendLine(line);
}
}
/* analyze and print execution plan */
function processQuery(query, explain, planIndex) {
'use strict';
@ -729,6 +792,7 @@ function processQuery(query, explain, planIndex) {
indexes = [],
traversalDetails = [],
shortestPathDetails = [],
kShortestPathsDetails = [],
functions = [],
modificationFlags,
isConst = true,
@ -1175,7 +1239,7 @@ function processQuery(query, explain, planIndex) {
});
return rc;
case 'ShortestPathNode':
case 'ShortestPathNode': {
if (node.hasOwnProperty('vertexOutVariable')) {
parts.push(variableName(node.vertexOutVariable) + ' ' + annotation('/* vertex */'));
}
@ -1183,7 +1247,7 @@ function processQuery(query, explain, planIndex) {
parts.push(variableName(node.edgeOutVariable) + ' ' + annotation('/* edge */'));
}
translate = ['ANY', 'INBOUND', 'OUTBOUND'];
var defaultDirection = node.directions[0];
let defaultDirection = node.directions[0];
rc = `${keyword("FOR")} ${parts.join(", ")} ${keyword("IN")} ${keyword(translate[defaultDirection])} ${keyword("SHORTEST_PATH")} `;
if (node.hasOwnProperty('startVertexId')) {
rc += `'${value(node.startVertexId)}'`;
@ -1237,6 +1301,67 @@ function processQuery(query, explain, planIndex) {
node.graph = '<anonymous>';
}
return rc;
}
case 'KShortestPathsNode': {
if (node.hasOwnProperty('pathOutVariable')) {
parts.push(variableName(node.pathOutVariable) + ' ' + annotation('/* path */'));
}
translate = ['ANY', 'INBOUND', 'OUTBOUND'];
let defaultDirection = node.directions[0];
rc = `${keyword("FOR")} ${parts.join(", ")} ${keyword("IN")} ${keyword(translate[defaultDirection])} ${keyword("K_SHORTEST_PATHS")} `;
if (node.hasOwnProperty('startVertexId')) {
rc += `'${value(node.startVertexId)}'`;
} else {
rc += variableName(node.startInVariable);
}
rc += ` ${annotation("/* startnode */")} ${keyword("TO")} `;
if (node.hasOwnProperty('targetVertexId')) {
rc += `'${value(node.targetVertexId)}'`;
} else {
rc += variableName(node.targetInVariable);
}
rc += ` ${annotation("/* targetnode */")} `;
if (Array.isArray(node.graph)) {
rc += node.graph.map(function (g, index) {
var tmp = '';
if (node.directions[index] !== defaultDirection) {
tmp += keyword(translate[node.directions[index]]);
tmp += ' ';
}
return tmp + collection(g);
}).join(', ');
} else {
rc += keyword('GRAPH') + " '" + value(node.graph) + "'";
}
kShortestPathsDetails.push(node);
e = [];
if (node.hasOwnProperty('graphDefinition')) {
v = [];
node.graphDefinition.vertexCollectionNames.forEach(function (vcn) {
v.push(collection(vcn));
});
node.vertexCollectionNameStr = v.join(', ');
node.vertexCollectionNameStrLen = node.graphDefinition.vertexCollectionNames.join(', ').length;
node.graphDefinition.edgeCollectionNames.forEach(function (ecn) {
e.push(collection(ecn));
});
node.edgeCollectionNameStr = e.join(', ');
node.edgeCollectionNameStrLen = node.graphDefinition.edgeCollectionNames.join(', ').length;
} else {
edgeCols = node.graph || [];
edgeCols.forEach(function (ecn) {
e.push(collection(ecn));
});
node.edgeCollectionNameStr = e.join(', ');
node.edgeCollectionNameStrLen = edgeCols.join(', ').length;
node.graph = '<anonymous>';
}
return rc;
}
case 'CalculationNode':
(node.functions || []).forEach(function (f) {
functions[f.name] = f;
@ -1596,6 +1721,7 @@ function processQuery(query, explain, planIndex) {
printFunctions(functions);
printTraversalDetails(traversalDetails);
printShortestPathDetails(shortestPathDetails);
printKShortestPathsDetails(kShortestPathsDetails);
stringBuilder.appendLine();
printRules(plan.rules);

View File

@ -172,6 +172,7 @@ set(ARANGODB_TESTS_SOURCES
Geo/ShapeContainerTest.cpp
Graph/ClusterTraverserCacheTest.cpp
Graph/ConstantWeightShortestPathFinder.cpp
Graph/KShortestPathsFinder.cpp
Maintenance/MaintenanceFeatureTest.cpp
Maintenance/MaintenanceRestHandlerTest.cpp
Maintenance/MaintenanceTest.cpp

View File

@ -0,0 +1,454 @@
////////////////////////////////////////////////////////////////////////////////
/// DISCLAIMER
///
/// Copyright 2019-2019 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 Markus Pfeiffer
////////////////////////////////////////////////////////////////////////////////
#include "catch.hpp"
#include "fakeit.hpp"
#include "Aql/AqlFunctionFeature.h"
#include "Aql/Ast.h"
#include "Aql/ExecutionPlan.h"
#include "Aql/OptimizerRulesFeature.h"
#include "Aql/Query.h"
#include "ClusterEngine/ClusterEngine.h"
#include "Graph/KShortestPathsFinder.h"
#include "Graph/ShortestPathOptions.h"
#include "Graph/ShortestPathResult.h"
#include "Random/RandomGenerator.h"
#include "RestServer/AqlFeature.h"
#include "RestServer/DatabaseFeature.h"
#include "RestServer/DatabasePathFeature.h"
#include "RestServer/QueryRegistryFeature.h"
#include "RestServer/SystemDatabaseFeature.h"
#include "RestServer/TraverserEngineRegistryFeature.h"
#include "StorageEngine/EngineSelectorFeature.h"
#include "Transaction/Methods.h"
#include "Transaction/StandaloneContext.h"
#include "Utils/SingleCollectionTransaction.h"
#include "VocBase/LogicalCollection.h"
#include <velocypack/Builder.h>
#include <velocypack/Slice.h>
#include <velocypack/velocypack-aliases.h>
// test setup
#include "../Mocks/Servers.h"
#include "../Mocks/StorageEngineMock.h"
#include "IResearch/common.h"
using namespace arangodb;
using namespace arangodb::aql;
using namespace arangodb::graph;
using namespace arangodb::velocypack;
namespace {
struct Setup {
StorageEngineMock engine;
arangodb::application_features::ApplicationServer server;
std::unique_ptr<TRI_vocbase_t> system;
std::vector<std::pair<arangodb::application_features::ApplicationFeature*, bool>> features;
Setup() : engine(server), server(nullptr, nullptr) {
arangodb::EngineSelectorFeature::ENGINE = &engine;
arangodb::transaction::Methods::clearDataSourceRegistrationCallbacks();
arangodb::ClusterEngine::Mocking = true;
arangodb::RandomGenerator::initialize(arangodb::RandomGenerator::RandomType::MERSENNE);
// suppress log messages since tests check error conditions
arangodb::LogTopic::setLogLevel(arangodb::Logger::FIXME.name(), arangodb::LogLevel::ERR); // suppress WARNING DefaultCustomTypeHandler called
// setup required application features
features.emplace_back(new arangodb::DatabasePathFeature(server), false);
features.emplace_back(new arangodb::DatabaseFeature(server), false);
features.emplace_back(new arangodb::QueryRegistryFeature(server), false); // must be first
arangodb::application_features::ApplicationServer::server->addFeature(
features.back().first); // need QueryRegistryFeature feature to be added now in order to create the system database
system = std::make_unique<TRI_vocbase_t>(TRI_vocbase_type_e::TRI_VOCBASE_TYPE_NORMAL,
0, TRI_VOC_SYSTEM_DATABASE);
features.emplace_back(new arangodb::SystemDatabaseFeature(server, system.get()),
false); // required for IResearchAnalyzerFeature
features.emplace_back(new arangodb::TraverserEngineRegistryFeature(server), false); // must be before AqlFeature
features.emplace_back(new arangodb::AqlFeature(server), true);
features.emplace_back(new arangodb::aql::OptimizerRulesFeature(server), true);
features.emplace_back(new arangodb::aql::AqlFunctionFeature(server), true); // required for IResearchAnalyzerFeature
for (auto& f : features) {
arangodb::application_features::ApplicationServer::server->addFeature(f.first);
}
for (auto& f : features) {
f.first->prepare();
}
for (auto& f : features) {
if (f.second) {
f.first->start();
}
}
auto* dbPathFeature =
arangodb::application_features::ApplicationServer::getFeature<arangodb::DatabasePathFeature>(
"DatabasePath");
arangodb::tests::setDatabasePath(*dbPathFeature); // ensure test data is stored in a unique directory
}
~Setup() {
system.reset(); // destroy before reseting the 'ENGINE'
arangodb::AqlFeature(server).stop(); // unset singleton instance
arangodb::LogTopic::setLogLevel(arangodb::Logger::FIXME.name(),
arangodb::LogLevel::DEFAULT);
arangodb::application_features::ApplicationServer::server = nullptr;
arangodb::EngineSelectorFeature::ENGINE = nullptr;
// destroy application features
for (auto& f : features) {
if (f.second) {
f.first->stop();
}
}
for (auto& f : features) {
f.first->unprepare();
}
}
}; // Setup
struct MockGraphDatabase {
TRI_vocbase_t vocbase;
std::vector<arangodb::aql::Query*> queries;
std::vector<arangodb::graph::ShortestPathOptions*> spos;
MockGraphDatabase(std::string name)
: vocbase(TRI_vocbase_type_e::TRI_VOCBASE_TYPE_NORMAL, 1, name) {}
~MockGraphDatabase() {
for (auto& q : queries) {
if (q->trx() != nullptr) {
q->trx()->abort();
}
delete q;
}
for (auto& o : spos) {
delete o;
}
}
// Create a collection with name <name> and <n> vertices
// with ids 0..n-1
void addVertexCollection(std::string name, size_t n) {
std::shared_ptr<arangodb::LogicalCollection> vertices;
auto createJson = velocypack::Parser::fromJson("{ \"name\": \"" + name +
"\", \"type\": 2 }");
vertices = vocbase.createCollection(createJson->slice());
REQUIRE((nullptr != vertices));
std::vector<velocypack::Builder> insertedDocs;
std::vector<std::shared_ptr<arangodb::velocypack::Builder>> docs;
for (size_t i = 0; i < n; i++) {
docs.emplace_back(arangodb::velocypack::Parser::fromJson(
"{ \"_key\": \"" + std::to_string(i) + "\"}"));
};
arangodb::OperationOptions options;
options.returnNew = true;
arangodb::SingleCollectionTransaction trx(arangodb::transaction::StandaloneContext::Create(vocbase),
*vertices, arangodb::AccessMode::Type::WRITE);
CHECK((trx.begin().ok()));
for (auto& entry : docs) {
auto res = trx.insert(vertices->name(), entry->slice(), options);
CHECK((res.ok()));
insertedDocs.emplace_back(res.slice().get("new"));
}
CHECK((trx.commit().ok()));
CHECK(insertedDocs.size() == n);
CHECK(vertices->type());
}
// Create a collection with name <name> of edges given by <edges>
void addEdgeCollection(std::string name, std::string vertexCollection,
std::vector<std::pair<size_t, size_t>> edgedef) {
std::shared_ptr<arangodb::LogicalCollection> edges;
auto createJson = velocypack::Parser::fromJson("{ \"name\": \"" + name +
"\", \"type\": 3 }");
edges = vocbase.createCollection(createJson->slice());
REQUIRE((nullptr != edges));
auto indexJson = velocypack::Parser::fromJson("{ \"type\": \"edge\" }");
bool created = false;
auto index = edges->createIndex(indexJson->slice(), created);
CHECK(index);
CHECK(created);
std::vector<velocypack::Builder> insertedDocs;
std::vector<std::shared_ptr<arangodb::velocypack::Builder>> docs;
for (auto& p : edgedef) {
auto docJson = velocypack::Parser::fromJson(
"{ \"_from\": \"" + vertexCollection + "/" + std::to_string(p.first) +
"\"" + ", \"_to\": \"" + vertexCollection + "/" +
std::to_string(p.second) + "\" }");
docs.emplace_back(docJson);
}
arangodb::OperationOptions options;
options.returnNew = true;
arangodb::SingleCollectionTransaction trx(arangodb::transaction::StandaloneContext::Create(vocbase),
*edges, arangodb::AccessMode::Type::WRITE);
CHECK((trx.begin().ok()));
for (auto& entry : docs) {
auto res = trx.insert(edges->name(), entry->slice(), options);
CHECK((res.ok()));
insertedDocs.emplace_back(res.slice().get("new"));
}
CHECK((trx.commit().ok()));
CHECK(insertedDocs.size() == edgedef.size());
}
arangodb::aql::Query* getQuery(std::string qry) {
auto queryString = arangodb::aql::QueryString(qry);
arangodb::aql::Query* query =
new arangodb::aql::Query(false, vocbase, queryString, nullptr,
arangodb::velocypack::Parser::fromJson("{}"),
arangodb::aql::PART_MAIN);
query->parse();
query->prepare(arangodb::QueryRegistryFeature::registry());
queries.emplace_back(query);
return query;
}
arangodb::graph::ShortestPathOptions* getShortestPathOptions(arangodb::aql::Query* query) {
arangodb::graph::ShortestPathOptions* spo;
auto plan = query->plan();
auto ast = plan->getAst();
auto _toCondition = ast->createNodeNaryOperator(NODE_TYPE_OPERATOR_NARY_AND);
auto _fromCondition = ast->createNodeNaryOperator(NODE_TYPE_OPERATOR_NARY_AND);
auto tmpVar = plan->getAst()->variables()->createTemporaryVariable();
AstNode* tmpId1 = plan->getAst()->createNodeReference(tmpVar);
AstNode* tmpId2 = plan->getAst()->createNodeValueString("", 0);
{
auto const* access =
ast->createNodeAttributeAccess(tmpId1, StaticStrings::ToString.c_str(),
StaticStrings::ToString.length());
auto const* cond =
ast->createNodeBinaryOperator(NODE_TYPE_OPERATOR_BINARY_EQ, access, tmpId2);
_toCondition->addMember(cond);
}
{
auto const* access =
ast->createNodeAttributeAccess(tmpId1, StaticStrings::FromString.c_str(),
StaticStrings::FromString.length());
auto const* cond =
ast->createNodeBinaryOperator(NODE_TYPE_OPERATOR_BINARY_EQ, access, tmpId2);
_fromCondition->addMember(cond);
}
spo = new ShortestPathOptions(query);
spo->setVariable(tmpVar);
spo->addLookupInfo(plan, "e", StaticStrings::FromString, _fromCondition->clone(ast));
spo->addReverseLookupInfo(plan, "e", StaticStrings::ToString, _toCondition->clone(ast));
spos.emplace_back(spo);
return spo;
}
};
} // namespace
namespace arangodb {
namespace tests {
namespace graph {
TEST_CASE("KShortestPathsFinder", "[graph]") {
Setup s;
UNUSED(s);
MockGraphDatabase gdb("testVocbase");
gdb.addVertexCollection("v", 100);
gdb.addEdgeCollection(
"e", "v",
{{1, 2}, {2, 3}, {3, 4}, {5, 4}, {6, 5}, {7, 6}, {8, 7},
{1, 10}, {10, 11}, {11, 12}, {12, 4}, {12, 5}, {21, 22}, {22, 23},
{23, 24}, {24, 25}, {21, 26}, {26, 27}, {27, 28}, {28, 25}, {30, 31},
{31, 32}, {32, 33}, {33, 34}, {34, 35}, {32, 30}, {33, 35}, {40, 41},
{41, 42}, {41, 43}, {42, 44}, {43, 44}, {44, 45}, {45, 46}, {46, 47},
{48, 47}, {49, 47}, {50, 47}, {48, 46}, {50, 46}, {50, 47}, {48, 46},
{50, 46}, {40, 60}, {60, 61}, {61, 62}, {62, 63}, {63, 64}, {64, 47},
{70, 71}, {70, 71}, {70, 71}});
auto query = gdb.getQuery("RETURN 1");
auto spo = gdb.getShortestPathOptions(query);
auto checkPath = [spo](ShortestPathResult result, std::vector<std::string> vertices,
std::vector<std::pair<std::string, std::string>> edges) -> bool {
bool res = true;
if (result.length() != vertices.size()) return false;
for (size_t i = 0; i < result.length(); i++) {
auto vert = result.vertexToAqlValue(spo->cache(), i);
if (!vert.slice().get(StaticStrings::KeyString).isEqualString(vertices.at(i))) {
LOG_DEVEL << "expected vertex " << vertices.at(i) << " but found "
<< vert.slice().get(StaticStrings::KeyString).toString();
res = false;
}
}
// First edge is by convention null
CHECK((result.edgeToAqlValue(spo->cache(), 0).isNull(true)));
for (size_t i = 1; i < result.length(); i++) {
auto edge = result.edgeToAqlValue(spo->cache(), i);
if (!edge.slice().get(StaticStrings::FromString).isEqualString(edges.at(i).first) ||
!edge.slice().get(StaticStrings::ToString).isEqualString(edges.at(i).second)) {
LOG_DEVEL << "expected edge " << edges.at(i).first << " -> "
<< edges.at(i).second << " but found "
<< edge.slice().get(StaticStrings::FromString).toString() << " -> "
<< edge.slice().get(StaticStrings::ToString).toString();
res = false;
}
}
return res;
};
auto finder = new KShortestPathsFinder(*spo);
SECTION("path from vertex to itself") {
auto start = velocypack::Parser::fromJson("\"v/0\"");
auto end = velocypack::Parser::fromJson("\"v/0\"");
ShortestPathResult result;
finder->startKShortestPathsTraversal(start->slice(), end->slice());
REQUIRE(true == finder->getNextPathShortestPathResult(result));
REQUIRE(false == finder->getNextPathShortestPathResult(result));
}
SECTION("no path exists") {
auto start = velocypack::Parser::fromJson("\"v/0\"");
auto end = velocypack::Parser::fromJson("\"v/1\"");
ShortestPathResult result;
finder->startKShortestPathsTraversal(start->slice(), end->slice());
REQUIRE(false == finder->getNextPathShortestPathResult(result));
// Repeat to see that we keep returning false and don't crash
REQUIRE(false == finder->getNextPathShortestPathResult(result));
}
SECTION("path of length 1") {
auto start = velocypack::Parser::fromJson("\"v/1\"");
auto end = velocypack::Parser::fromJson("\"v/2\"");
ShortestPathResult result;
finder->startKShortestPathsTraversal(start->slice(), end->slice());
REQUIRE(true == finder->getNextPathShortestPathResult(result));
REQUIRE(checkPath(result, {"1", "2"}, {{}, {"v/1", "v/2"}}));
}
SECTION("path of length 4") {
auto start = velocypack::Parser::fromJson("\"v/1\"");
auto end = velocypack::Parser::fromJson("\"v/4\"");
ShortestPathResult result;
finder->startKShortestPathsTraversal(start->slice(), end->slice());
REQUIRE(true == finder->getNextPathShortestPathResult(result));
REQUIRE(checkPath(result, {"1", "2", "3", "4"},
{{}, {"v/1", "v/2"}, {"v/2", "v/3"}, {"v/3", "v/4"}}));
}
SECTION("path of length 5 with loops to start/end") {
auto start = velocypack::Parser::fromJson("\"v/30\"");
auto end = velocypack::Parser::fromJson("\"v/35\"");
ShortestPathResult result;
finder->startKShortestPathsTraversal(start->slice(), end->slice());
REQUIRE(true == finder->getNextPathShortestPathResult(result));
REQUIRE(result.length() == 5);
}
SECTION("two paths of length 5") {
auto start = velocypack::Parser::fromJson("\"v/21\"");
auto end = velocypack::Parser::fromJson("\"v/25\"");
ShortestPathResult result;
finder->startKShortestPathsTraversal(start->slice(), end->slice());
REQUIRE(true == finder->getNextPathShortestPathResult(result));
CHECK((checkPath(result, {"21", "22", "23", "24", "25"},
{{},
{"v/21", "v/22"},
{"v/22", "v/23"},
{"v/23", "v/24"},
{"v/24", "v/25"}}) ||
checkPath(result, {"21", "26", "27", "28", "25"},
{{},
{"v/21", "v/26"},
{"v/26", "v/27"},
{"v/27", "v/28"},
{"v/28", "v/25"}})));
REQUIRE(true == finder->getNextPathShortestPathResult(result));
CHECK((checkPath(result, {"21", "22", "23", "24", "25"},
{{},
{"v/21", "v/22"},
{"v/22", "v/23"},
{"v/23", "v/24"},
{"v/24", "v/25"}}) ||
checkPath(result, {"21", "26", "27", "28", "25"},
{{},
{"v/21", "v/26"},
{"v/26", "v/27"},
{"v/27", "v/28"},
{"v/28", "v/25"}})));
}
SECTION("many edges between two nodes") {
auto start = velocypack::Parser::fromJson("\"v/70\"");
auto end = velocypack::Parser::fromJson("\"v/71\"");
ShortestPathResult result;
finder->startKShortestPathsTraversal(start->slice(), end->slice());
REQUIRE(true == finder->getNextPathShortestPathResult(result));
REQUIRE(true == finder->getNextPathShortestPathResult(result));
REQUIRE(true == finder->getNextPathShortestPathResult(result));
REQUIRE(false == finder->getNextPathShortestPathResult(result));
}
delete finder;
}
} // namespace graph
} // namespace tests
} // namespace arangodb

View File

@ -0,0 +1,531 @@
/*jshint globalstrict:false, strict:false, sub: true, maxlen: 500 */
/*global assertEqual, assertTrue, assertFalse */
////////////////////////////////////////////////////////////////////////////////
/// @brief tests for query language, graph functions
///
/// @file
///
/// DISCLAIMER
///
/// Copyright 2010-2012 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 Michael Hackstein
/// @author Copyright 2018, ArangoDB GmbH, Cologne, Germany
////////////////////////////////////////////////////////////////////////////////
const jsunity = require("jsunity");
const db = require("@arangodb").db;
const gm = require("@arangodb/general-graph");
const _ = require("underscore");
const graphName = "UnitTestGraph";
const vName = "UnitTestVertices";
const e1Name = "UnitTestEdges1";
const e2Name = "UnitTestEdges2";
const source = `${vName}/source`;
const target = `${vName}/target`;
const badTarget = `${vName}/badTarget`;
const looper = `${vName}/looper`;
const isPathValid = (path, length, expectedWeight, allowInbound = false) => {
assertTrue(_.isObject(path));
// Assert all attributes are present
assertTrue(path.hasOwnProperty("vertices"));
assertTrue(path.hasOwnProperty("edges"));
assertTrue(path.hasOwnProperty("weight"));
// Assert weight and number of edges are correct
const { vertices, edges, weight } = path;
assertEqual(edges.length, length);
assertEqual(weight, expectedWeight);
assertEqual(edges.length + 1, vertices.length);
// Assert that source and target are correct
assertEqual(vertices[0]._id, source);
assertEqual(vertices[vertices.length - 1]._id, target);
// Assert that the edges and vertices are in correct order
// And assert that we have not found a vertex twice
const seenVertices = new Set();
for (let i = 0; i < vertices.length - 1; ++i) {
const from = vertices[i]._id;
assertFalse(seenVertices.has(from), `Found vertex ${from} twice on path: ${path.vertices.map(v => v._id)}`);
seenVertices.add(from);
const to = vertices[i + 1]._id;
// Do not ADD to, it will be added next step, just make sure to test both
// for the ends
assertFalse(seenVertices.has(to), `Found vertex ${to} twice on path: ${path.vertices.map(v => v._id)}`);
const e = edges[i];
if (e._from === from) {
// OUTBOUND EDGE
assertEqual(e._to, to);
} else {
// INBOUND EDGE
assertEqual(e._to, from);
assertEqual(e._from, to);
assertTrue(allowInbound);
}
}
};
const allPathsDiffer = (paths) => {
const seenPath = new Set();
for (const p of paths) {
const stringP = JSON.stringify(p);
assertFalse(seenPath.has(stringP), `Found path ${stringP} twice ${JSON.stringify(paths, null, 2)}`);
seenPath.add(stringP);
}
};
const allPathsAreSorted = (paths) => {
let last = paths[0].weight;
for (const p of paths) {
assertTrue(last <= p.weight);
last = p.weight;
}
};
const tearDownAll = () => {
try {
gm._drop(graphName, true);
} catch (e) {
// Don't care for error, we might runinitially with no graph exist
}
db._drop(e2Name);
db._drop(e1Name);
db._drop(vName);
};
const createGraph = () => {
gm._create(graphName, [
gm._relation(e1Name, vName, vName),
gm._relation(e2Name, vName, vName)
], [],
{
numberOfShards: 9
}
);
const vertices = [];
const e1s = [];
const e2s = [];
vertices.push({
_key: "source"
});
vertices.push({
_key: "target"
});
vertices.push({
_key: "badTarget"
});
vertices.push({ _key: "looper" });
// Insert Data:
// Pathnum 0 - 2 are relevant for result.
// Pathnum 3 - 5 are noise on the start vertex
// Pathnum 6 - 8 are noise on the target vertex
// Pathnum 9 - 11 are noise on the isolated vertex
for (let pathNum = 0; pathNum < 12; ++pathNum) {
const weight = (pathNum + 1) * (pathNum + 1);
for (let step = 0; step < 3; ++step) {
const key = `vertex_${pathNum}_${step}`;
vertices.push({ _key: key });
// Add valid edges:
switch (step) {
case 0: {
if (pathNum < 6) {
// source -> v
e1s.push({ _from: source, _to: `${vName}/${key}`, weight });
if (pathNum < 3) {
// Add INBOUND shortcut 0 <- 2 in e2 (we intentionally go to path 6-8 to not interfer with the original paths)
e2s.push({ _from: `${vName}/vertex_${pathNum + 6}_0`, _to: `${vName}/${key}`, weight });
}
} else if (pathNum < 9) {
// v -> target
e1s.push({ _from: `${vName}/${key}`, _to: target, weight });
} else {
// v-> bad
e1s.push({ _from: `${vName}/${key}`, _to: badTarget, weight });
}
break;
}
case 1: {
// connect to step 0
e1s.push({ _from: `${vName}/vertex_${pathNum}_0`, _to: `${vName}/${key}`, weight });
const mod = pathNum % 3;
if (mod !== 0) {
// Connect to the path before
e1s.push({ _from: `${vName}/vertex_${pathNum - 1}_0`, _to: `${vName}/${key}`, weight });
}
if (mod !== 2) {
// Connect to the path after
e1s.push({ _from: `${vName}/vertex_${pathNum + 1}_0`, _to: `${vName}/${key}`, weight });
}
if (mod === 2 && pathNum === 2) {
// Add a path loop and a duplicate edge
// duplicate edge
e1s.push({ _from: `${vName}/vertex_${pathNum}_0`, _to: `${vName}/${key}`, weight: weight + 1 });
e1s.push({ _from: `${vName}/${key}`, _to: looper, weight });
e1s.push({ _from: looper, _to: `${vName}/vertex_${pathNum}_0`, weight });
}
break;
}
case 2: {
if (pathNum < 3) {
// These are the valid paths we care for
if (pathNum === 1) {
const additional = `vertex_${pathNum}_3`;
vertices.push({ _key: additional });
// Add an aditional step only on the second path to have differnt path lengths
e1s.push({ _from: `${vName}/${key}`, _to: `${vName}/${additional}`, weight });
e1s.push({ _from: `${vName}/${additional}`, _to: target, weight });
} else {
e1s.push({ _from: `${vName}/${key}`, _to: target, weight });
}
}
// Always connect to source:
// 1 -> 2 is connected in e2
e2s.push({ _from: `${vName}/vertex_${pathNum}_1`, _to: `${vName}/${key}`, weight });
break;
}
}
}
}
db[vName].save(vertices);
db[e1Name].save(e1s);
db[e2Name].save(e2s);
// This graph has the following features:
// we have 5 paths source -> target of length 4 (via postfix of path0 and path2)
// we have 3 paths source -> target of length 5 (via postfix of path1)
// we have 1 path source -> target of length 8 (S,V1_0,V2_1,Loop,V2_0,V1_1,V1_2,V1_3,T)
// The weights are defined as (1 + pathNum)^2 on every edge (the duplicate on path2 is 10 instead of 9).
// So we end up with weight on the following paths:
// * 4 on path0 (4 edges)
// * 7 path1->path0 (4 edges)
// * 17 path0->path1 (5 edges)
// * 20 path1 (5edges)
// * 25 path2->path1 (5 edges)
// * 31 path1->path2 (4 edges)
// * 36 path2 (4 edges)
// * 37 path2 alt(4 edges)
// * 47 on loop path (8 edges)
// We hav no paths source -> badTarget
// We have 4 paths when e2 is traversed inbound instead of outbound
// source -e1> <e2- target of length 3
// source -e1> loop -e1> 0_2 <e2- t of length 6
// So we end up with weight on the following paths:
// * 51 on path0
// * 72 on path1
// * 99 on path2
// * 121 on loop
};
function kConstantWeightShortestPathTestSuite() {
return {
setUpAll: function () {
tearDownAll();
createGraph();
},
tearDownAll,
testPathsExistsLimit: function () {
const query = `
FOR path IN OUTBOUND K_SHORTEST_PATHS "${source}" TO "${target}" GRAPH "${graphName}"
LIMIT 6
RETURN path
`;
const result = db._query(query).toArray();
allPathsDiffer(result);
assertEqual(result.length, 6);
allPathsAreSorted(result);
for (let i = 0; i < 5; ++i) {
isPathValid(result[i], 4, 4);
}
// The 6th path is the only longer one
isPathValid(result[5], 5, 5);
},
testNoPathExistsLimit: function () {
const query = `
FOR path IN OUTBOUND K_SHORTEST_PATHS "${source}" TO "${badTarget}" GRAPH "${graphName}"
LIMIT 6
RETURN path
`;
const result = db._query(query).toArray();
assertEqual(result.length, 0);
},
testFewerPathsThanLimit: function () {
const query = `
FOR path IN OUTBOUND K_SHORTEST_PATHS "${source}" TO "${target}" GRAPH "${graphName}"
LIMIT 1000
RETURN path
`;
const result = db._query(query).toArray();
allPathsDiffer(result);
assertEqual(result.length, 9);
allPathsAreSorted(result);
for (let i = 0; i < 5; ++i) {
isPathValid(result[i], 4, 4);
}
for (let i = 5; i < 8; ++i) {
isPathValid(result[i], 5, 5);
}
isPathValid(result[8], 8, 8);
},
testPathsExistsNoLimit: function () {
const query = `
FOR source IN ${vName}
FILTER source._id == "${source}"
FOR target IN ${vName}
FILTER target._id == "${target}"
FOR path IN OUTBOUND K_SHORTEST_PATHS source TO target GRAPH "${graphName}"
RETURN path
`;
const result = db._query(query).toArray();
allPathsDiffer(result);
assertEqual(result.length, 9);
allPathsAreSorted(result);
for (let i = 0; i < 5; ++i) {
isPathValid(result[i], 4, 4);
}
for (let i = 5; i < 8; ++i) {
isPathValid(result[i], 5, 5);
}
isPathValid(result[8], 8, 8);
},
testNoPathsExistsNoLimit: function () {
const query = `
FOR source IN ${vName}
FILTER source._id == "${source}"
FOR target IN ${vName}
FILTER target._id == "${badTarget}"
FOR path IN OUTBOUND K_SHORTEST_PATHS source TO target GRAPH "${graphName}"
RETURN path
`;
const result = db._query(query).toArray();
assertEqual(result.length, 0);
},
testPathsSkip: function () {
const query = `
FOR path IN OUTBOUND K_SHORTEST_PATHS "${source}" TO "${target}" GRAPH "${graphName}"
LIMIT 3, 3
RETURN path
`;
const result = db._query(query).toArray();
allPathsDiffer(result);
assertEqual(result.length, 3);
allPathsAreSorted(result);
for (let i = 0; i < 2; ++i) {
isPathValid(result[i], 4, 4);
}
for (let i = 2; i < 3; ++i) {
isPathValid(result[i], 5, 5);
}
},
testPathsSkipMoreThanExists: function () {
const query = `
FOR path IN OUTBOUND K_SHORTEST_PATHS "${source}" TO "${target}" GRAPH "${graphName}"
LIMIT 1000, 2
RETURN path
`;
const result = db._query(query).toArray();
assertEqual(result.length, 0);
},
testMultiDirections: function () {
const query = `
WITH ${vName}
FOR path IN OUTBOUND K_SHORTEST_PATHS "${source}" TO "${target}" ${e1Name}, INBOUND ${e2Name}
RETURN path
`;
const result = db._query(query).toArray();
allPathsDiffer(result);
assertEqual(result.length, 4);
allPathsAreSorted(result);
for (let i = 0; i < 3; ++i) {
isPathValid(result[i], 3, 3, true);
}
isPathValid(result[3], 6, 6, true);
}
};
}
function kAttributeWeightShortestPathTestSuite() {
return {
setUpAll: function () {
tearDownAll();
createGraph();
},
tearDownAll,
testWeightPathsExistsLimit: function () {
const query = `
FOR path IN OUTBOUND K_SHORTEST_PATHS "${source}" TO "${target}" GRAPH "${graphName}" OPTIONS {weightAttribute: "weight"}
LIMIT 6
RETURN path
`;
const result = db._query(query).toArray();
allPathsDiffer(result);
assertEqual(result.length, 6);
allPathsAreSorted(result);
isPathValid(result[0], 4, 4);
isPathValid(result[1], 4, 7);
isPathValid(result[2], 5, 17);
isPathValid(result[3], 5, 20);
isPathValid(result[4], 5, 25);
isPathValid(result[5], 4, 31);
},
testWeightNoPathExistsLimit: function () {
const query = `
FOR path IN OUTBOUND K_SHORTEST_PATHS "${source}" TO "${badTarget}" GRAPH "${graphName}" OPTIONS {weightAttribute: "weight"}
LIMIT 6
RETURN path
`;
const result = db._query(query).toArray();
assertEqual(result.length, 0);
},
testWeightFewerPathsThanLimit: function () {
const query = `
FOR path IN OUTBOUND K_SHORTEST_PATHS "${source}" TO "${target}" GRAPH "${graphName}" OPTIONS {weightAttribute: "weight"}
LIMIT 1000
RETURN path
`;
const result = db._query(query).toArray();
allPathsDiffer(result);
assertEqual(result.length, 9);
allPathsAreSorted(result);
isPathValid(result[0], 4, 4);
isPathValid(result[1], 4, 7);
isPathValid(result[2], 5, 17);
isPathValid(result[3], 5, 20);
isPathValid(result[4], 5, 25);
isPathValid(result[5], 4, 31);
isPathValid(result[6], 4, 36);
isPathValid(result[7], 4, 37);
isPathValid(result[8], 8, 47);
},
testWeightPathsExistsNoLimit: function () {
const query = `
FOR source IN ${vName}
FILTER source._id == "${source}"
FOR target IN ${vName}
FILTER target._id == "${target}"
FOR path IN OUTBOUND K_SHORTEST_PATHS source TO target GRAPH "${graphName}" OPTIONS {weightAttribute: "weight"}
RETURN path
`;
const result = db._query(query).toArray();
allPathsDiffer(result);
assertEqual(result.length, 9);
allPathsAreSorted(result);
isPathValid(result[0], 4, 4);
isPathValid(result[1], 4, 7);
isPathValid(result[2], 5, 17);
isPathValid(result[3], 5, 20);
isPathValid(result[4], 5, 25);
isPathValid(result[5], 4, 31);
isPathValid(result[6], 4, 36);
isPathValid(result[7], 4, 37);
isPathValid(result[8], 8, 47);
},
testWeightNoPathsExistsNoLimit: function () {
const query = `
FOR source IN ${vName}
FILTER source._id == "${source}"
FOR target IN ${vName}
FILTER target._id == "${badTarget}"
FOR path IN OUTBOUND K_SHORTEST_PATHS source TO target GRAPH "${graphName}" OPTIONS {weightAttribute: "weight"}
RETURN path
`;
const result = db._query(query).toArray();
assertEqual(result.length, 0);
},
testWeightPathsSkip: function () {
const query = `
FOR path IN OUTBOUND K_SHORTEST_PATHS "${source}" TO "${target}" GRAPH "${graphName}" OPTIONS {weightAttribute: "weight"}
LIMIT 3, 3
RETURN path
`;
const result = db._query(query).toArray();
allPathsDiffer(result);
assertEqual(result.length, 3);
allPathsAreSorted(result);
isPathValid(result[0], 5, 20);
isPathValid(result[1], 5, 25);
isPathValid(result[2], 4, 31);
},
testWeightPathsSkipMoreThanExists: function () {
const query = `
FOR path IN OUTBOUND K_SHORTEST_PATHS "${source}" TO "${target}" GRAPH "${graphName}" OPTIONS {weightAttribute: "weight"}
LIMIT 1000, 2
RETURN path
`;
const result = db._query(query).toArray();
assertEqual(result.length, 0);
},
testWeightMultiDirections: function () {
const query = `
WITH ${vName}
FOR path IN OUTBOUND K_SHORTEST_PATHS "${source}" TO "${target}" ${e1Name}, INBOUND ${e2Name} OPTIONS {weightAttribute: "weight"}
RETURN path
`;
const result = db._query(query).toArray();
allPathsDiffer(result);
assertEqual(result.length, 4);
allPathsAreSorted(result);
isPathValid(result[0], 3, 51, true);
isPathValid(result[1], 3, 72, true);
isPathValid(result[2], 3, 99, true);
isPathValid(result[3], 6, 121, true);
}
};
}
jsunity.run(kConstantWeightShortestPathTestSuite);
jsunity.run(kAttributeWeightShortestPathTestSuite);
return jsunity.done();