Add ExpandWeightedShortestPathCursor
Summary: Implement Dijkstras algorithm to find weighted shortest path. Reviewers: teon.banek, florijan, buda Reviewed By: teon.banek Subscribers: pullbot, mtomic Differential Revision: https://phabricator.memgraph.io/D1182
This commit is contained in:
parent
4326847ab3
commit
b1a8e48dc4
docs/user_technical
src/query/plan
tests
qa/tck_engine/tests/memgraph_V1/features
unit
@ -470,10 +470,10 @@ To find only the shortest path, simply append `LIMIT 1` to the `RETURN` clause.
|
||||
|
||||
MATCH (a {id: 723})-[r:Type *bfs..10]-(b {id: 882}) RETURN * LIMIT 1
|
||||
|
||||
Breadth-fist expansion allows an arbitrary expression filter that determines
|
||||
Breadth-first expansion allows an arbitrary expression filter that determines
|
||||
if an expansion is allowed. Following is an example in which expansion is
|
||||
allowed only over edges whose `x` property is greater then `12` and nodes `y`
|
||||
whose property is lesser then `3`:
|
||||
allowed only over edges whose `x` property is greater than `12` and nodes `y`
|
||||
whose property is less than `3`:
|
||||
|
||||
MATCH (a {id: 723})-[*bfs..10 (e, n | e.x > 12 and n.y < 3)]-() RETURN *
|
||||
|
||||
@ -490,6 +490,40 @@ neighbourhood in breadth-first manner.
|
||||
Currently, it isn't possible to get all shortest paths to a single node using
|
||||
Memgraph's breadth-first expansion.
|
||||
|
||||
#### Weighted Shortest Path
|
||||
|
||||
Another standard use-case in a graph is searching for the weighted shortest
|
||||
path between nodes. The openCypher standard does not define this feature, so
|
||||
Memgraph provides a custom implementation, based on the edge expansion syntax.
|
||||
|
||||
Finding the weighted shortest path between nodes is done using the weighted
|
||||
shortest path expansion:
|
||||
|
||||
MATCH (a {id: 723})-[le *wShortest 10 (e, n | e.weight) total_weight]-(b {id: 882}) RETURN *
|
||||
|
||||
The above query will find the shortest path of length up to 10 nodes between
|
||||
nodes `a` and `b`.
|
||||
|
||||
Weighted Shortest Path expansion allows an arbitrary expression that determines
|
||||
the weight for the current expansion. Total weight of a path is calculated as
|
||||
the sum of all weights on the path between two nodes. Following is an example in
|
||||
which the weight between nodes is defined as the product of edge weights
|
||||
(instead of sum), assuming all weights are greater than '1':
|
||||
|
||||
MATCH (a {id: 723})-[le *wShortest 10 (e, n | log(e.weight)) total_weight]-(b {id: 882}) RETURN exp(total_weight)
|
||||
|
||||
|
||||
Weighted Shortest Path expansions also allows an arbitrary expression filter
|
||||
that determines if an expansion is allowed. Following is an example in which
|
||||
expansion is allowed only over edges whose `x` property is greater than `12`
|
||||
and nodes `y` whose property is less than `3`:
|
||||
|
||||
MATCH (a {id: 723})-[le *wShortest 10 (e, n | e.weight) total_weight (e, n | e.x > 12 and n.y < 3)]-(b {id: 882}) RETURN exp(total_weight)
|
||||
|
||||
Both weight and filter expression are defined as lambda functions over `e` and
|
||||
`n`, which denote the edge and the node being expanded over in the weighted
|
||||
shortest path search.
|
||||
|
||||
#### UNWIND
|
||||
|
||||
The `UNWIND` clause is used to unwind a list of values as individual rows.
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
#include <queue>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
@ -322,10 +323,10 @@ std::unique_ptr<Cursor> ScanAllByLabelPropertyRange::MakeCursor(
|
||||
context.symbol_table_, db, graph_view_);
|
||||
auto convert = [&evaluator](const auto &bound)
|
||||
-> std::experimental::optional<utils::Bound<PropertyValue>> {
|
||||
if (!bound) return std::experimental::nullopt;
|
||||
return std::experimental::make_optional(utils::Bound<PropertyValue>(
|
||||
bound.value().value()->Accept(evaluator), bound.value().type()));
|
||||
};
|
||||
if (!bound) return std::experimental::nullopt;
|
||||
return std::experimental::make_optional(utils::Bound<PropertyValue>(
|
||||
bound.value().value()->Accept(evaluator), bound.value().type()));
|
||||
};
|
||||
return db.Vertices(label_, property_, convert(lower_bound()),
|
||||
convert(upper_bound()), graph_view_ == GraphView::NEW);
|
||||
};
|
||||
@ -573,20 +574,23 @@ ExpandVariable::ExpandVariable(
|
||||
const std::vector<storage::EdgeType> &edge_types, bool is_reverse,
|
||||
Expression *lower_bound, Expression *upper_bound,
|
||||
const std::shared_ptr<LogicalOperator> &input, Symbol input_symbol,
|
||||
bool existing_node, Symbol inner_edge_symbol, Symbol inner_node_symbol,
|
||||
Expression *filter, GraphView graph_view)
|
||||
bool existing_node, Lambda filter_lambda,
|
||||
std::experimental::optional<Lambda> weight_lambda,
|
||||
std::experimental::optional<Symbol> total_weight, GraphView graph_view)
|
||||
: ExpandCommon(node_symbol, edge_symbol, direction, edge_types, input,
|
||||
input_symbol, existing_node, graph_view),
|
||||
type_(type),
|
||||
is_reverse_(is_reverse),
|
||||
lower_bound_(lower_bound),
|
||||
upper_bound_(upper_bound),
|
||||
inner_edge_symbol_(inner_edge_symbol),
|
||||
inner_node_symbol_(inner_node_symbol),
|
||||
filter_(filter) {
|
||||
filter_lambda_(filter_lambda),
|
||||
weight_lambda_(weight_lambda),
|
||||
total_weight_(total_weight) {
|
||||
DCHECK(type_ == EdgeAtom::Type::DEPTH_FIRST ||
|
||||
type_ == EdgeAtom::Type::BREADTH_FIRST)
|
||||
<< "ExpandVariable can only be used with breadth or depth first type";
|
||||
type_ == EdgeAtom::Type::BREADTH_FIRST ||
|
||||
type_ == EdgeAtom::Type::WEIGHTED_SHORTEST_PATH)
|
||||
<< "ExpandVariable can only be used with breadth first, depth first or "
|
||||
"weighted shortest path type";
|
||||
DCHECK(!(type_ == EdgeAtom::Type::BREADTH_FIRST && is_reverse))
|
||||
<< "Breadth first expansion can't be reversed";
|
||||
}
|
||||
@ -855,9 +859,11 @@ class ExpandVariableCursor : public Cursor {
|
||||
if (!self_.HandleExistingNode(current_vertex, frame)) continue;
|
||||
|
||||
// Skip expanding out of filtered expansion.
|
||||
frame[self_.inner_edge_symbol_] = current_edge.first;
|
||||
frame[self_.inner_node_symbol_] = current_vertex;
|
||||
if (self_.filter_ && !EvaluateFilter(evaluator, self_.filter_)) continue;
|
||||
frame[self_.filter_lambda_.inner_edge_symbol] = current_edge.first;
|
||||
frame[self_.filter_lambda_.inner_node_symbol] = current_vertex;
|
||||
if (self_.filter_lambda_.expression &&
|
||||
!EvaluateFilter(evaluator, self_.filter_lambda_.expression))
|
||||
continue;
|
||||
|
||||
// we are doing depth-first search, so place the current
|
||||
// edge's expansions onto the stack, if we should continue to expand
|
||||
@ -899,11 +905,11 @@ class ExpandBreadthFirstCursor : public query::plan::Cursor {
|
||||
SwitchAccessor(edge, self_.graph_view_);
|
||||
SwitchAccessor(vertex, self_.graph_view_);
|
||||
|
||||
frame[self_.inner_edge_symbol_] = edge;
|
||||
frame[self_.inner_node_symbol_] = vertex;
|
||||
frame[self_.filter_lambda_.inner_edge_symbol] = edge;
|
||||
frame[self_.filter_lambda_.inner_node_symbol] = vertex;
|
||||
|
||||
if (self_.filter_) {
|
||||
TypedValue result = self_.filter_->Accept(evaluator);
|
||||
if (self_.filter_lambda_.expression) {
|
||||
TypedValue result = self_.filter_lambda_.expression->Accept(evaluator);
|
||||
switch (result.type()) {
|
||||
case TypedValue::Type::Null:
|
||||
return;
|
||||
@ -1035,10 +1041,211 @@ class ExpandBreadthFirstCursor : public query::plan::Cursor {
|
||||
std::deque<std::pair<EdgeAccessor, VertexAccessor>> to_visit_next_;
|
||||
};
|
||||
|
||||
class ExpandWeightedShortestPathCursor : public query::plan::Cursor {
|
||||
public:
|
||||
ExpandWeightedShortestPathCursor(const ExpandVariable &self,
|
||||
database::GraphDbAccessor &db)
|
||||
: self_(self), db_(db), input_cursor_(self_.input_->MakeCursor(db)) {}
|
||||
|
||||
bool Pull(Frame &frame, Context &context) override {
|
||||
ExpressionEvaluator evaluator(frame, context.parameters_,
|
||||
context.symbol_table_, db_,
|
||||
self_.graph_view_);
|
||||
// For the given (vertex, edge, vertex) tuple checks if they satisfy the
|
||||
// "where" condition. if so, places them in the priority queue.
|
||||
auto expand_pair = [this, &evaluator, &frame](
|
||||
VertexAccessor from, EdgeAccessor edge, VertexAccessor vertex) {
|
||||
SwitchAccessor(edge, self_.graph_view_);
|
||||
SwitchAccessor(vertex, self_.graph_view_);
|
||||
|
||||
if (self_.filter_lambda_.expression) {
|
||||
frame[self_.filter_lambda_.inner_edge_symbol] = edge;
|
||||
frame[self_.filter_lambda_.inner_node_symbol] = vertex;
|
||||
|
||||
if (!EvaluateFilter(evaluator, self_.filter_lambda_.expression)) return;
|
||||
}
|
||||
|
||||
frame[self_.weight_lambda_->inner_edge_symbol] = edge;
|
||||
frame[self_.weight_lambda_->inner_node_symbol] = vertex;
|
||||
|
||||
TypedValue typed_weight =
|
||||
self_.weight_lambda_->expression->Accept(evaluator);
|
||||
|
||||
if (!typed_weight.IsNumeric()) {
|
||||
throw QueryRuntimeException("Calculated weight must be numeric, got {}",
|
||||
typed_weight.type());
|
||||
}
|
||||
if ((typed_weight < 0).Value<bool>()) {
|
||||
throw QueryRuntimeException("Calculated weight can't be negative!");
|
||||
}
|
||||
|
||||
auto total_weight = weights_[from] + typed_weight;
|
||||
auto found_it = weights_.find(vertex);
|
||||
if (found_it != weights_.end() &&
|
||||
found_it->second.Value<double>() <= total_weight.Value<double>())
|
||||
return;
|
||||
|
||||
pq_.push(std::make_pair(std::make_pair(vertex, edge),
|
||||
total_weight.Value<double>()));
|
||||
};
|
||||
|
||||
// Populates the priority queue structure with expansions
|
||||
// from the given vertex. skips expansions that don't satisfy
|
||||
// the "where" condition.
|
||||
auto expand_from_vertex = [this, &expand_pair](VertexAccessor &vertex) {
|
||||
if (self_.direction_ != EdgeAtom::Direction::IN) {
|
||||
for (const EdgeAccessor &edge : vertex.out(&self_.edge_types_)) {
|
||||
expand_pair(vertex, edge, edge.to());
|
||||
}
|
||||
}
|
||||
if (self_.direction_ != EdgeAtom::Direction::OUT) {
|
||||
for (const EdgeAccessor &edge : vertex.in(&self_.edge_types_)) {
|
||||
expand_pair(vertex, edge, edge.from());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
while (true) {
|
||||
if (pq_.empty()) {
|
||||
if (!input_cursor_->Pull(frame, context)) return false;
|
||||
auto vertex_value = frame[self_.input_symbol_];
|
||||
if (vertex_value.IsNull()) continue;
|
||||
auto vertex = vertex_value.Value<VertexAccessor>();
|
||||
if (self_.existing_node_) {
|
||||
TypedValue &node = frame[self_.node_symbol_];
|
||||
// Due to optional matching the existing node could be null.
|
||||
// Skip expansion for such nodes.
|
||||
if (node.IsNull()) continue;
|
||||
}
|
||||
SwitchAccessor(vertex, self_.graph_view_);
|
||||
upper_bound_ =
|
||||
self_.upper_bound_
|
||||
? EvaluateInt(evaluator, self_.upper_bound_,
|
||||
"Max depth in weighted shortest path expansion")
|
||||
: std::numeric_limits<int>::max();
|
||||
if (upper_bound_ < 1)
|
||||
throw QueryRuntimeException(
|
||||
"Max depth in weighted shortest path expansion must be greater "
|
||||
"than zero");
|
||||
|
||||
// Clear existing data structures.
|
||||
previous_.clear();
|
||||
weights_.clear();
|
||||
|
||||
pq_.push(std::make_pair(
|
||||
std::make_pair(vertex, std::experimental::nullopt), 0.0));
|
||||
}
|
||||
|
||||
while (!pq_.empty()) {
|
||||
auto current = pq_.top();
|
||||
pq_.pop();
|
||||
|
||||
// Check if the edge has already been processed.
|
||||
if (weights_.find(current.first.first) != weights_.end()) {
|
||||
continue;
|
||||
}
|
||||
previous_.emplace(current.first.first, current.first.second);
|
||||
weights_.emplace(current.first.first, current.second);
|
||||
|
||||
// Reconstruct the path.
|
||||
auto last_vertex = current.first.first;
|
||||
std::vector<TypedValue> edge_list{};
|
||||
while (true) {
|
||||
// Origin_vertex must be in previous.
|
||||
const auto &previous_edge = previous_.find(last_vertex)->second;
|
||||
if (!previous_edge) break;
|
||||
last_vertex = previous_edge->from() == last_vertex
|
||||
? previous_edge->to()
|
||||
: previous_edge->from();
|
||||
edge_list.push_back(previous_edge.value());
|
||||
}
|
||||
|
||||
// Expand only if what we've just expanded is less then max depth.
|
||||
if (static_cast<int>(edge_list.size()) < upper_bound_)
|
||||
expand_from_vertex(current.first.first);
|
||||
|
||||
if (edge_list.empty()) continue;
|
||||
|
||||
// Place destination node on the frame, handle existence flag.
|
||||
if (self_.existing_node_) {
|
||||
TypedValue &node = frame[self_.node_symbol_];
|
||||
if ((node != current.first.first).Value<bool>())
|
||||
continue;
|
||||
else
|
||||
// Prevent expanding other paths, because we found the
|
||||
// shortest to existing node.
|
||||
ClearQueue();
|
||||
} else {
|
||||
frame[self_.node_symbol_] = current.first.first;
|
||||
}
|
||||
|
||||
if (!self_.is_reverse_) {
|
||||
// Place edges on the frame in the correct order.
|
||||
std::reverse(edge_list.begin(), edge_list.end());
|
||||
}
|
||||
frame[self_.edge_symbol_] = std::move(edge_list);
|
||||
frame[self_.total_weight_.value()] = current.second;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Reset() override {
|
||||
input_cursor_->Reset();
|
||||
previous_.clear();
|
||||
weights_.clear();
|
||||
ClearQueue();
|
||||
}
|
||||
|
||||
private:
|
||||
const ExpandVariable &self_;
|
||||
database::GraphDbAccessor &db_;
|
||||
const std::unique_ptr<query::plan::Cursor> input_cursor_;
|
||||
|
||||
// Upper bound on the path length.
|
||||
int upper_bound_{-1};
|
||||
|
||||
// Maps vertices to weights they got in expansion.
|
||||
std::unordered_map<VertexAccessor, TypedValue> weights_;
|
||||
|
||||
// Maps vertices to edges used to reach them.
|
||||
std::unordered_map<VertexAccessor, std::experimental::optional<EdgeAccessor>>
|
||||
previous_;
|
||||
|
||||
// Priority queue comparator. Keep lowest weight on top of the queue.
|
||||
class PriorityQueueComparator {
|
||||
public:
|
||||
bool operator()(
|
||||
const std::pair<std::pair<VertexAccessor,
|
||||
std::experimental::optional<EdgeAccessor>>,
|
||||
double> &lhs,
|
||||
const std::pair<std::pair<VertexAccessor,
|
||||
std::experimental::optional<EdgeAccessor>>,
|
||||
double> &rhs) {
|
||||
return lhs.second > rhs.second;
|
||||
}
|
||||
};
|
||||
std::priority_queue<
|
||||
std::pair<
|
||||
std::pair<VertexAccessor, std::experimental::optional<EdgeAccessor>>,
|
||||
double>,
|
||||
std::vector<std::pair<
|
||||
std::pair<VertexAccessor, std::experimental::optional<EdgeAccessor>>,
|
||||
double>>,
|
||||
PriorityQueueComparator>
|
||||
pq_;
|
||||
|
||||
void ClearQueue() {
|
||||
while (!pq_.empty()) pq_.pop();
|
||||
}
|
||||
};
|
||||
|
||||
std::unique_ptr<Cursor> ExpandVariable::MakeCursor(
|
||||
database::GraphDbAccessor &db) const {
|
||||
if (type_ == EdgeAtom::Type::BREADTH_FIRST)
|
||||
return std::make_unique<ExpandBreadthFirstCursor>(*this, db);
|
||||
else if (type_ == EdgeAtom::Type::WEIGHTED_SHORTEST_PATH)
|
||||
return std::make_unique<ExpandWeightedShortestPathCursor>(*this, db);
|
||||
else
|
||||
return std::make_unique<ExpandVariableCursor>(*this, db);
|
||||
}
|
||||
@ -1597,8 +1804,8 @@ bool ExpandUniquenessFilter<TAccessor>::ExpandUniquenessFilterCursor::Pull(
|
||||
for (const auto &previous_symbol : self_.previous_symbols_) {
|
||||
TypedValue &previous_value = frame[previous_symbol];
|
||||
// This shouldn't raise a TypedValueException, because the planner
|
||||
// makes sure these are all of the expected type. In case they are not, an
|
||||
// error should be raised long before this code is executed.
|
||||
// makes sure these are all of the expected type. In case they are not,
|
||||
// an error should be raised long before this code is executed.
|
||||
if (ContainsSame<TAccessor>(previous_value, expand_value)) return false;
|
||||
}
|
||||
return true;
|
||||
@ -2626,8 +2833,8 @@ bool PullRemote::PullRemoteCursor::Pull(Frame &frame, Context &context) {
|
||||
|
||||
if (!have_remote_results) {
|
||||
// If we didn't find any remote results and there aren't any remote
|
||||
// pulls, we've exhausted all remote results. Make sure we signal that to
|
||||
// workers and exit the loop.
|
||||
// pulls, we've exhausted all remote results. Make sure we signal that
|
||||
// to workers and exit the loop.
|
||||
if (remote_pulls_.empty()) {
|
||||
EndRemotePull();
|
||||
break;
|
||||
@ -2659,8 +2866,8 @@ bool PullRemote::PullRemoteCursor::Pull(Frame &frame, Context &context) {
|
||||
remote_results_[pull_from_worker_id].resize(
|
||||
remote_results_[pull_from_worker_id].size() - 1);
|
||||
|
||||
// Remove the worker if we exhausted all locally stored results and there are
|
||||
// no more pending remote pulls for that worker.
|
||||
// Remove the worker if we exhausted all locally stored results and there
|
||||
// are no more pending remote pulls for that worker.
|
||||
if (remote_results_[pull_from_worker_id].empty() &&
|
||||
remote_pulls_.find(pull_from_worker_id) == remote_pulls_.end()) {
|
||||
worker_ids_.erase(worker_ids_.begin() + last_pulled_worker_id_index_);
|
||||
|
@ -807,8 +807,33 @@ class ExpandVariable : public LogicalOperator, public ExpandCommon {
|
||||
// that should be inaccessible (private class function won't compile)
|
||||
friend class ExpandVariableCursor;
|
||||
friend class ExpandBreadthFirstCursor;
|
||||
friend class ExpandWeightedShortestPathCursor;
|
||||
|
||||
public:
|
||||
struct Lambda {
|
||||
// Symbols for a single node and edge that are currently getting expanded.
|
||||
Symbol inner_edge_symbol;
|
||||
Symbol inner_node_symbol;
|
||||
// Expression used in lambda during expansion.
|
||||
Expression *expression;
|
||||
|
||||
BOOST_SERIALIZATION_SPLIT_MEMBER();
|
||||
|
||||
template <class TArchive>
|
||||
void save(TArchive &ar, const unsigned int) const {
|
||||
ar &inner_edge_symbol;
|
||||
ar &inner_node_symbol;
|
||||
SavePointer(ar, expression);
|
||||
}
|
||||
|
||||
template <class TArchive>
|
||||
void load(TArchive &ar, const unsigned int) {
|
||||
ar &inner_edge_symbol;
|
||||
ar &inner_node_symbol;
|
||||
LoadPointer(ar, expression);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a variable-length expansion. Most params are forwarded
|
||||
* to the @c ExpandCommon constructor, and are documented there.
|
||||
@ -838,9 +863,9 @@ class ExpandVariable : public LogicalOperator, public ExpandCommon {
|
||||
bool is_reverse, Expression *lower_bound,
|
||||
Expression *upper_bound,
|
||||
const std::shared_ptr<LogicalOperator> &input,
|
||||
Symbol input_symbol, bool existing_node,
|
||||
Symbol inner_edge_symbol, Symbol inner_node_symbol,
|
||||
Expression *filter = nullptr,
|
||||
Symbol input_symbol, bool existing_node, Lambda filter_lambda,
|
||||
std::experimental::optional<Lambda> weight_lambda,
|
||||
std::experimental::optional<Symbol> total_weight,
|
||||
GraphView graph_view = GraphView::AS_IS);
|
||||
|
||||
bool Accept(HierarchicalLogicalOperatorVisitor &visitor) override;
|
||||
@ -858,12 +883,10 @@ class ExpandVariable : public LogicalOperator, public ExpandCommon {
|
||||
// both are optional, defaults are (1, inf)
|
||||
Expression *lower_bound_;
|
||||
Expression *upper_bound_;
|
||||
// symbols for a single node and edge that are currently getting expanded
|
||||
Symbol inner_edge_symbol_;
|
||||
Symbol inner_node_symbol_;
|
||||
// a filtering expression for skipping expansions during expansion
|
||||
// can refer to inner node and edges
|
||||
Expression *filter_;
|
||||
|
||||
Lambda filter_lambda_;
|
||||
std::experimental::optional<Lambda> weight_lambda_;
|
||||
std::experimental::optional<Symbol> total_weight_;
|
||||
|
||||
ExpandVariable() {}
|
||||
|
||||
@ -879,9 +902,9 @@ class ExpandVariable : public LogicalOperator, public ExpandCommon {
|
||||
ar &is_reverse_;
|
||||
SavePointer(ar, lower_bound_);
|
||||
SavePointer(ar, upper_bound_);
|
||||
ar &inner_edge_symbol_;
|
||||
ar &inner_node_symbol_;
|
||||
SavePointer(ar, filter_);
|
||||
ar &filter_lambda_;
|
||||
ar &weight_lambda_;
|
||||
ar &total_weight_;
|
||||
}
|
||||
|
||||
template <class TArchive>
|
||||
@ -892,9 +915,9 @@ class ExpandVariable : public LogicalOperator, public ExpandCommon {
|
||||
ar &is_reverse_;
|
||||
LoadPointer(ar, lower_bound_);
|
||||
LoadPointer(ar, upper_bound_);
|
||||
ar &inner_edge_symbol_;
|
||||
ar &inner_node_symbol_;
|
||||
LoadPointer(ar, filter_);
|
||||
ar &filter_lambda_;
|
||||
ar &weight_lambda_;
|
||||
ar &total_weight_;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -352,24 +352,37 @@ class RuleBasedPlanner {
|
||||
DCHECK(!utils::Contains(bound_symbols, edge_symbol))
|
||||
<< "Existing edges are not supported";
|
||||
if (edge->IsVariable()) {
|
||||
Symbol inner_edge_symbol =
|
||||
std::experimental::optional<ExpandVariable::Lambda> weight_lambda;
|
||||
std::experimental::optional<Symbol> total_weight;
|
||||
|
||||
if (edge->type_ == EdgeAtom::Type::WEIGHTED_SHORTEST_PATH) {
|
||||
weight_lambda.emplace(ExpandVariable::Lambda{
|
||||
symbol_table.at(*edge->weight_lambda_.inner_edge),
|
||||
symbol_table.at(*edge->weight_lambda_.inner_node),
|
||||
edge->weight_lambda_.expression});
|
||||
|
||||
total_weight.emplace(symbol_table.at(*edge->total_weight_));
|
||||
}
|
||||
|
||||
ExpandVariable::Lambda filter_lambda;
|
||||
filter_lambda.inner_edge_symbol =
|
||||
symbol_table.at(*edge->filter_lambda_.inner_edge);
|
||||
Symbol inner_node_symbol =
|
||||
filter_lambda.inner_node_symbol =
|
||||
symbol_table.at(*edge->filter_lambda_.inner_node);
|
||||
{
|
||||
// Bind the inner edge and node symbols so they're available for
|
||||
// inline filtering in ExpandVariable.
|
||||
bool inner_edge_bound =
|
||||
bound_symbols.insert(inner_edge_symbol).second;
|
||||
bound_symbols.insert(filter_lambda.inner_edge_symbol).second;
|
||||
bool inner_node_bound =
|
||||
bound_symbols.insert(inner_node_symbol).second;
|
||||
bound_symbols.insert(filter_lambda.inner_node_symbol).second;
|
||||
DCHECK(inner_edge_bound && inner_node_bound)
|
||||
<< "An inner edge and node can't be bound from before";
|
||||
}
|
||||
// Join regular filters with lambda filter expression, so that they
|
||||
// are done inline together. Semantic analysis should guarantee that
|
||||
// lambda filtering uses bound symbols.
|
||||
auto *filter_expr = impl::BoolJoin<AndOperator>(
|
||||
filter_lambda.expression = impl::BoolJoin<AndOperator>(
|
||||
storage, impl::ExtractFilters(bound_symbols, filters, storage),
|
||||
edge->filter_lambda_.expression);
|
||||
// At this point it's possible we have leftover filters for inline
|
||||
@ -378,22 +391,24 @@ class RuleBasedPlanner {
|
||||
// will ever bind them again.
|
||||
filters.erase(
|
||||
std::remove_if(filters.begin(), filters.end(),
|
||||
[ e = inner_edge_symbol,
|
||||
n = inner_node_symbol ](FilterInfo & fi) {
|
||||
[
|
||||
e = filter_lambda.inner_edge_symbol,
|
||||
n = filter_lambda.inner_node_symbol
|
||||
](FilterInfo & fi) {
|
||||
return utils::Contains(fi.used_symbols, e) ||
|
||||
utils::Contains(fi.used_symbols, n);
|
||||
}),
|
||||
filters.end());
|
||||
// Unbind the temporarily bound inner symbols for filtering.
|
||||
bound_symbols.erase(inner_edge_symbol);
|
||||
bound_symbols.erase(inner_node_symbol);
|
||||
bound_symbols.erase(filter_lambda.inner_edge_symbol);
|
||||
bound_symbols.erase(filter_lambda.inner_node_symbol);
|
||||
|
||||
// TODO: Pass weight lambda.
|
||||
last_op = std::make_unique<ExpandVariable>(
|
||||
node_symbol, edge_symbol, edge->type_, expansion.direction,
|
||||
edge->edge_types_, expansion.is_flipped, edge->lower_bound_,
|
||||
edge->upper_bound_, std::move(last_op), node1_symbol,
|
||||
existing_node, inner_edge_symbol, inner_node_symbol, filter_expr,
|
||||
existing_node, filter_lambda, weight_lambda, total_weight,
|
||||
match_context.graph_view);
|
||||
} else {
|
||||
if (!existing_node) {
|
||||
|
@ -0,0 +1,119 @@
|
||||
Feature: Weighted Shortest Path
|
||||
|
||||
Scenario: Test match wShortest upper bound
|
||||
Given an empty graph
|
||||
And having executed:
|
||||
"""
|
||||
CREATE (n {a:'0'})-[:r {w: 1}]->({a:'1'})-[:r {w: 1}]->({a:'2'}), (n)-[:r {w: 1}]->({a:'3'})
|
||||
"""
|
||||
When executing query:
|
||||
"""
|
||||
MATCH (n {a:'0'})-[le *wShortest 1 (e, n | e.w ) w]->(m) RETURN m.a
|
||||
"""
|
||||
Then the result should be:
|
||||
| m.a |
|
||||
| '1' |
|
||||
| '3' |
|
||||
|
||||
Scenario: Test match wShortest filtered
|
||||
Given an empty graph
|
||||
And having executed:
|
||||
"""
|
||||
CREATE (n {a:'0'})-[:r {w: 1}]->({a:'1'})-[:r {w: 1}]->({a:'2'}), (n)-[:r {w: 1}]->({a:'3'})
|
||||
"""
|
||||
When executing query:
|
||||
"""
|
||||
MATCH (n {a:'0'})-[le *wShortest 1 (e, n | e.w ) w (e, n | n.a = '3')]->(m) RETURN m.a
|
||||
"""
|
||||
Then the result should be:
|
||||
| m.a |
|
||||
| '3' |
|
||||
|
||||
Scenario: Test match wShortest resulting edge list
|
||||
Given an empty graph
|
||||
And having executed:
|
||||
"""
|
||||
CREATE (n {a:'0'})-[:r {w: 1}]->({a:'1'})-[:r {w: 2}]->({a:'2'}), (n)-[:r {w: 4}]->({a:'3'})
|
||||
"""
|
||||
When executing query:
|
||||
"""
|
||||
MATCH (n {a:'0'})-[le *wShortest 10 (e, n | e.w ) w]->(m) RETURN m.a, size(le) as s, w
|
||||
"""
|
||||
Then the result should be:
|
||||
| m.a | s | w |
|
||||
| '1' | 1 | 1.0 |
|
||||
| '2' | 2 | 3.0 |
|
||||
| '3' | 1 | 4.0 |
|
||||
|
||||
Scenario: Test match wShortest single edge type filtered
|
||||
Given an empty graph
|
||||
And having executed:
|
||||
"""
|
||||
CREATE (n {a:'0'})-[:r0 {w: 1}]->({a:'1'})-[:r {w: 2}]->({a:'2'}), (n)-[:r {w: 3}]->({a:'4'})
|
||||
"""
|
||||
When executing query:
|
||||
"""
|
||||
MATCH ()-[le:r0 *wShortest 10 (e, n | e.w) w]->(m)
|
||||
RETURN size(le) AS s, m.a
|
||||
"""
|
||||
Then the result should be:
|
||||
| s | m.a |
|
||||
| 1 | '1' |
|
||||
|
||||
Scenario: Test match wShortest multiple edge types filtered
|
||||
Given an empty graph
|
||||
And having executed:
|
||||
"""
|
||||
CREATE (n {a:'0'})-[:r0 {w: 1}]->({a:'1'})-[:r1 {w: 2}]->({a:'2'}), (n)-[:r {w: 3}]->({a:'4'})
|
||||
"""
|
||||
When executing query:
|
||||
"""
|
||||
MATCH ()-[le :r0|:r1 *wShortest 10 (e, n | e.w) w]->(m) WHERE size(le) > 1
|
||||
RETURN size(le) AS s, (le[0]).w AS r0, (le[1]).w AS r1
|
||||
"""
|
||||
Then the result should be:
|
||||
| s | r0 | r1 |
|
||||
| 2 | 1 | 2 |
|
||||
|
||||
Scenario: Test match wShortest property filters
|
||||
Given an empty graph
|
||||
And having executed:
|
||||
"""
|
||||
CREATE (n {a:'0'})-[:r {w: 1}]->({a:'1'})-[:r {w: 2}]->({a:'2'}), (n)-[:r {w: 3}]->({a:'4'})
|
||||
"""
|
||||
When executing query:
|
||||
"""
|
||||
MATCH ()-[le *wShortest 10 {w:1} (e, n | e.w ) total_weight]->(m)
|
||||
RETURN size(le) AS s, (le[0]).w AS r0
|
||||
"""
|
||||
Then the result should be:
|
||||
| s | r0 |
|
||||
| 1 | 1 |
|
||||
|
||||
Scenario: Test match wShortest weight not a number
|
||||
Given an empty graph
|
||||
And having executed:
|
||||
"""
|
||||
CREATE (n {a:'0'})-[:r {w: 'not a number'}]->({a:'1'})-[:r {w: 2}]->({a:'2'}), (n)-[:r {w: 3}]->({a:'4'})
|
||||
"""
|
||||
When executing query:
|
||||
"""
|
||||
MATCH ()-[le *wShortest 10 (e, n | e.w ) total_weight]->(m)
|
||||
RETURN le, total_weight
|
||||
"""
|
||||
Then an error should be raised
|
||||
|
||||
Scenario: Test match wShortest negative weight
|
||||
Given an empty graph
|
||||
And having executed:
|
||||
"""
|
||||
CREATE (n {a:'0'})-[:r {w: -1}]->({a:'1'})-[:r {w: 2}]->({a:'2'}), (n)-[:r {w: 3}]->({a:'4'})
|
||||
"""
|
||||
When executing query:
|
||||
"""
|
||||
MATCH ()-[le *wShortest 10 (e, n | e.w ) total_weight]->(m)
|
||||
RETURN le, total_weight
|
||||
"""
|
||||
Then an error should be raised
|
||||
|
||||
|
@ -250,3 +250,51 @@ TEST_F(InterpreterTest, CreateIndexInMulticommandTransaction) {
|
||||
interpreter_("CREATE INDEX ON :X(y)", dba, {}, true).PullAll(stream),
|
||||
query::IndexInMulticommandTxException);
|
||||
}
|
||||
|
||||
// Test shortest path end to end.
|
||||
TEST_F(InterpreterTest, ShortestPath) {
|
||||
{
|
||||
ResultStreamFaker stream;
|
||||
database::GraphDbAccessor dba(db_);
|
||||
interpreter_(
|
||||
"CREATE (n:A {x: 1}), (m:B {x: 2}), (l:C {x: 1}), (n)-[:r1 {w: 1 "
|
||||
"}]->(m)-[:r2 {w: 2}]->(l), (n)-[:r3 {w: 4}]->(l)",
|
||||
dba, {}, true)
|
||||
.PullAll(stream);
|
||||
|
||||
dba.Commit();
|
||||
}
|
||||
|
||||
ResultStreamFaker stream;
|
||||
database::GraphDbAccessor dba(db_);
|
||||
interpreter_("MATCH (n)-[e *wshortest 5 (e, n | e.w) ]->(m) return e", dba,
|
||||
{}, false)
|
||||
.PullAll(stream);
|
||||
|
||||
ASSERT_EQ(stream.GetHeader().size(), 1U);
|
||||
EXPECT_EQ(stream.GetHeader()[0], "e");
|
||||
ASSERT_EQ(stream.GetResults().size(), 3U);
|
||||
|
||||
std::vector<std::vector<std::string>> expected_results{
|
||||
{"r1"}, {"r2"}, {"r1", "r2"}};
|
||||
|
||||
for (const auto &result : stream.GetResults()) {
|
||||
const auto &edges =
|
||||
query::test_common::ToList<EdgeAccessor>(result[0].ValueList());
|
||||
|
||||
std::vector<std::string> datum;
|
||||
for (const auto &edge : edges) {
|
||||
datum.push_back(dba.EdgeTypeName(edge.EdgeType()));
|
||||
}
|
||||
|
||||
bool any_match = false;
|
||||
for (const auto &expected : expected_results) {
|
||||
if (expected == datum) {
|
||||
any_match = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
EXPECT_TRUE(any_match);
|
||||
}
|
||||
}
|
||||
|
@ -168,11 +168,12 @@ TEST_F(QueryCostEstimator, Expand) {
|
||||
}
|
||||
|
||||
TEST_F(QueryCostEstimator, ExpandVariable) {
|
||||
MakeOp<ExpandVariable>(NextSymbol(), NextSymbol(),
|
||||
EdgeAtom::Type::DEPTH_FIRST, EdgeAtom::Direction::IN,
|
||||
std::vector<storage::EdgeType>{}, false, nullptr,
|
||||
nullptr, last_op_, NextSymbol(), false, NextSymbol(),
|
||||
NextSymbol(), nullptr);
|
||||
MakeOp<ExpandVariable>(
|
||||
NextSymbol(), NextSymbol(), EdgeAtom::Type::DEPTH_FIRST,
|
||||
EdgeAtom::Direction::IN, std::vector<storage::EdgeType>{}, false, nullptr,
|
||||
nullptr, last_op_, NextSymbol(), false,
|
||||
ExpandVariable::Lambda{NextSymbol(), NextSymbol(), nullptr},
|
||||
std::experimental::nullopt, std::experimental::nullopt);
|
||||
EXPECT_COST(CardParam::kExpandVariable * CostParam::kExpandVariable);
|
||||
}
|
||||
|
||||
|
@ -429,11 +429,15 @@ class QueryPlanExpandVariable : public testing::Test {
|
||||
auto convert = [this](std::experimental::optional<size_t> bound) {
|
||||
return bound ? LITERAL(static_cast<int64_t>(bound.value())) : nullptr;
|
||||
};
|
||||
|
||||
return std::make_shared<ExpandVariable>(
|
||||
n_to_sym, edge_sym, EdgeAtom::Type::DEPTH_FIRST, direction,
|
||||
edge_types, is_reverse, convert(lower), convert(upper), filter_op,
|
||||
n_from.sym_, false, symbol_table.CreateSymbol("inner_edge", false),
|
||||
symbol_table.CreateSymbol("inner_node", false), nullptr, graph_view);
|
||||
n_from.sym_, false,
|
||||
ExpandVariable::Lambda{symbol_table.CreateSymbol("inner_edge", false),
|
||||
symbol_table.CreateSymbol("inner_node", false),
|
||||
nullptr},
|
||||
std::experimental::nullopt, std::experimental::nullopt, graph_view);
|
||||
} else
|
||||
return std::make_shared<Expand>(n_to_sym, edge_sym, direction, edge_types,
|
||||
filter_op, n_from.sym_, false,
|
||||
@ -763,11 +767,14 @@ class QueryPlanExpandBreadthFirst : public testing::Test {
|
||||
? existing_node_input->sym_
|
||||
: symbol_table.CreateSymbol("node", true);
|
||||
auto edge_list_sym = symbol_table.CreateSymbol("edgelist_", true);
|
||||
last_op = std::make_shared<ExpandVariable>(
|
||||
node_sym, edge_list_sym, EdgeAtom::Type::BREADTH_FIRST, direction,
|
||||
std::vector<storage::EdgeType>{}, false, nullptr, LITERAL(max_depth),
|
||||
last_op, n.sym_, existing_node_input != nullptr, inner_edge, inner_node,
|
||||
where, graph_view);
|
||||
auto filter_lambda =
|
||||
|
||||
last_op = std::make_shared<ExpandVariable>(
|
||||
node_sym, edge_list_sym, EdgeAtom::Type::BREADTH_FIRST, direction,
|
||||
std::vector<storage::EdgeType>{}, false, nullptr,
|
||||
LITERAL(max_depth), last_op, n.sym_, existing_node_input != nullptr,
|
||||
ExpandVariable::Lambda{inner_edge, inner_node, where},
|
||||
std::experimental::nullopt, std::experimental::nullopt, graph_view);
|
||||
|
||||
Frame frame(symbol_table.max_position());
|
||||
auto cursor = last_op->MakeCursor(dba);
|
||||
@ -913,6 +920,333 @@ TEST_F(QueryPlanExpandBreadthFirst, ExistingNode) {
|
||||
}
|
||||
}
|
||||
|
||||
/** A test fixture for weighted shortest path expansion */
|
||||
class QueryPlanExpandWeightedShortestPath : public testing::Test {
|
||||
public:
|
||||
struct ResultType {
|
||||
std::vector<EdgeAccessor> path;
|
||||
VertexAccessor vertex;
|
||||
double total_weight;
|
||||
};
|
||||
|
||||
protected:
|
||||
// style-guide non-conformant name due to PROPERTY_PAIR and PROPERTY_LOOKUP
|
||||
// macro requirements
|
||||
database::SingleNode db;
|
||||
database::GraphDbAccessor dba{db};
|
||||
std::pair<std::string, storage::Property> prop = PROPERTY_PAIR("property");
|
||||
storage::EdgeType edge_type = dba.EdgeType("edge_type");
|
||||
|
||||
// make 5 vertices because we'll need to compare against them exactly
|
||||
// v[0] has `prop` with the value 0
|
||||
std::vector<VertexAccessor> v;
|
||||
|
||||
// make some edges too, in a map (from, to) vertex indices
|
||||
std::unordered_map<std::pair<int, int>, EdgeAccessor> e;
|
||||
|
||||
AstTreeStorage storage;
|
||||
SymbolTable symbol_table;
|
||||
|
||||
// inner edge and vertex symbols
|
||||
Symbol filter_edge = symbol_table.CreateSymbol("f_edge", true);
|
||||
Symbol filter_node = symbol_table.CreateSymbol("f_node", true);
|
||||
|
||||
Symbol weight_edge = symbol_table.CreateSymbol("w_edge", true);
|
||||
Symbol weight_node = symbol_table.CreateSymbol("w_node", true);
|
||||
|
||||
Symbol total_weight = symbol_table.CreateSymbol("total_weight", true);
|
||||
|
||||
void SetUp() {
|
||||
for (int i = 0; i < 5; i++) {
|
||||
v.push_back(dba.InsertVertex());
|
||||
v.back().PropsSet(prop.second, i);
|
||||
}
|
||||
|
||||
auto add_edge = [&](int from, int to, double weight) {
|
||||
EdgeAccessor edge = dba.InsertEdge(v[from], v[to], edge_type);
|
||||
edge.PropsSet(prop.second, weight);
|
||||
e.emplace(std::make_pair(from, to), edge);
|
||||
};
|
||||
|
||||
add_edge(0, 1, 5);
|
||||
add_edge(1, 4, 5);
|
||||
add_edge(0, 2, 3);
|
||||
add_edge(2, 3, 3);
|
||||
add_edge(3, 4, 3);
|
||||
add_edge(4, 0, 12);
|
||||
|
||||
dba.AdvanceCommand();
|
||||
for (auto &vertex : v) vertex.Reconstruct();
|
||||
for (auto &edge : e) edge.second.Reconstruct();
|
||||
}
|
||||
|
||||
// defines and performs a breadth-first expansion with the given params
|
||||
// returns a vector of pairs. each pair is (vector-of-edges, vertex)
|
||||
auto ExpandWShortest(EdgeAtom::Direction direction, int max_depth,
|
||||
Expression *where,
|
||||
GraphView graph_view = GraphView::AS_IS,
|
||||
std::experimental::optional<int> node_id = 0,
|
||||
ScanAllTuple *existing_node_input = nullptr) {
|
||||
// scan the nodes optionally filtering on property value
|
||||
auto n =
|
||||
MakeScanAll(storage, symbol_table, "n",
|
||||
existing_node_input ? existing_node_input->op_ : nullptr);
|
||||
auto last_op = n.op_;
|
||||
if (node_id) {
|
||||
last_op = std::make_shared<Filter>(
|
||||
last_op,
|
||||
EQ(PROPERTY_LOOKUP(n.node_->identifier_, prop), LITERAL(*node_id)));
|
||||
}
|
||||
|
||||
auto ident_e = IDENT("e");
|
||||
symbol_table[*ident_e] = weight_edge;
|
||||
|
||||
// expand wshortest
|
||||
auto node_sym = existing_node_input
|
||||
? existing_node_input->sym_
|
||||
: symbol_table.CreateSymbol("node", true);
|
||||
auto edge_list_sym = symbol_table.CreateSymbol("edgelist_", true);
|
||||
auto filter_lambda = last_op = std::make_shared<ExpandVariable>(
|
||||
node_sym, edge_list_sym, EdgeAtom::Type::WEIGHTED_SHORTEST_PATH,
|
||||
direction, std::vector<storage::EdgeType>{}, false, nullptr,
|
||||
LITERAL(max_depth), last_op, n.sym_, existing_node_input != nullptr,
|
||||
ExpandVariable::Lambda{filter_edge, filter_node, where},
|
||||
ExpandVariable::Lambda{weight_edge, weight_node,
|
||||
PROPERTY_LOOKUP(ident_e, prop)},
|
||||
total_weight, graph_view);
|
||||
|
||||
Frame frame(symbol_table.max_position());
|
||||
auto cursor = last_op->MakeCursor(dba);
|
||||
std::vector<ResultType> results;
|
||||
Context context(dba);
|
||||
context.symbol_table_ = symbol_table;
|
||||
while (cursor->Pull(frame, context)) {
|
||||
results.push_back(ResultType{std::vector<EdgeAccessor>(),
|
||||
frame[node_sym].Value<VertexAccessor>(),
|
||||
frame[total_weight].Value<double>()});
|
||||
for (const TypedValue &edge : frame[edge_list_sym].ValueList())
|
||||
results.back().path.emplace_back(edge.Value<EdgeAccessor>());
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
template <typename TAccessor>
|
||||
auto GetProp(const TAccessor &accessor) {
|
||||
return accessor.PropsAt(prop.second).template Value<int64_t>();
|
||||
}
|
||||
|
||||
template <typename TAccessor>
|
||||
auto GetDoubleProp(const TAccessor &accessor) {
|
||||
return accessor.PropsAt(prop.second).template Value<double>();
|
||||
}
|
||||
|
||||
Expression *PropNe(Symbol symbol, int value) {
|
||||
auto ident = IDENT("inner_element");
|
||||
symbol_table[*ident] = symbol;
|
||||
return NEQ(PROPERTY_LOOKUP(ident, prop), LITERAL(value));
|
||||
}
|
||||
};
|
||||
|
||||
// Testing weighted shortest path on this graph:
|
||||
//
|
||||
// 5 5
|
||||
// /-->--[1]-->--\
|
||||
// / \
|
||||
// / 12 \ 2
|
||||
// [0]--------<--------[4]------->-------[5]
|
||||
// \ / (only for GraphState test)
|
||||
// \ /
|
||||
// \->[2]->-[3]->/
|
||||
// 3 3 3
|
||||
|
||||
TEST_F(QueryPlanExpandWeightedShortestPath, Basic) {
|
||||
auto results =
|
||||
ExpandWShortest(EdgeAtom::Direction::BOTH, 1000, LITERAL(true));
|
||||
|
||||
ASSERT_EQ(results.size(), 4);
|
||||
|
||||
// check end nodes
|
||||
EXPECT_EQ(GetProp(results[0].vertex), 2);
|
||||
EXPECT_EQ(GetProp(results[1].vertex), 1);
|
||||
EXPECT_EQ(GetProp(results[2].vertex), 3);
|
||||
EXPECT_EQ(GetProp(results[3].vertex), 4);
|
||||
|
||||
// check paths and total weights
|
||||
EXPECT_EQ(results[0].path.size(), 1);
|
||||
EXPECT_EQ(GetDoubleProp(results[0].path[0]), 3);
|
||||
EXPECT_EQ(results[0].total_weight, 3);
|
||||
|
||||
EXPECT_EQ(results[1].path.size(), 1);
|
||||
EXPECT_EQ(GetDoubleProp(results[1].path[0]), 5);
|
||||
EXPECT_EQ(results[1].total_weight, 5);
|
||||
|
||||
EXPECT_EQ(results[2].path.size(), 2);
|
||||
EXPECT_EQ(GetDoubleProp(results[2].path[0]), 3);
|
||||
EXPECT_EQ(GetDoubleProp(results[2].path[1]), 3);
|
||||
EXPECT_EQ(results[2].total_weight, 6);
|
||||
|
||||
EXPECT_EQ(results[3].path.size(), 3);
|
||||
EXPECT_EQ(GetDoubleProp(results[3].path[0]), 3);
|
||||
EXPECT_EQ(GetDoubleProp(results[3].path[1]), 3);
|
||||
EXPECT_EQ(GetDoubleProp(results[3].path[2]), 3);
|
||||
EXPECT_EQ(results[3].total_weight, 9);
|
||||
}
|
||||
|
||||
TEST_F(QueryPlanExpandWeightedShortestPath, EdgeDirection) {
|
||||
{
|
||||
auto results =
|
||||
ExpandWShortest(EdgeAtom::Direction::OUT, 1000, LITERAL(true));
|
||||
ASSERT_EQ(results.size(), 4);
|
||||
EXPECT_EQ(GetProp(results[0].vertex), 2);
|
||||
EXPECT_EQ(results[0].total_weight, 3);
|
||||
EXPECT_EQ(GetProp(results[1].vertex), 1);
|
||||
EXPECT_EQ(results[1].total_weight, 5);
|
||||
EXPECT_EQ(GetProp(results[2].vertex), 3);
|
||||
EXPECT_EQ(results[2].total_weight, 6);
|
||||
EXPECT_EQ(GetProp(results[3].vertex), 4);
|
||||
EXPECT_EQ(results[3].total_weight, 9);
|
||||
}
|
||||
{
|
||||
auto results =
|
||||
ExpandWShortest(EdgeAtom::Direction::IN, 1000, LITERAL(true));
|
||||
ASSERT_EQ(results.size(), 4);
|
||||
EXPECT_EQ(GetProp(results[0].vertex), 4);
|
||||
EXPECT_EQ(results[0].total_weight, 12);
|
||||
EXPECT_EQ(GetProp(results[1].vertex), 3);
|
||||
EXPECT_EQ(results[1].total_weight, 15);
|
||||
EXPECT_EQ(GetProp(results[2].vertex), 1);
|
||||
EXPECT_EQ(results[2].total_weight, 17);
|
||||
EXPECT_EQ(GetProp(results[3].vertex), 2);
|
||||
EXPECT_EQ(results[3].total_weight, 18);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(QueryPlanExpandWeightedShortestPath, Where) {
|
||||
{
|
||||
auto results = ExpandWShortest(EdgeAtom::Direction::BOTH, 1000,
|
||||
PropNe(filter_node, 2));
|
||||
ASSERT_EQ(results.size(), 3);
|
||||
EXPECT_EQ(GetProp(results[0].vertex), 1);
|
||||
EXPECT_EQ(results[0].total_weight, 5);
|
||||
EXPECT_EQ(GetProp(results[1].vertex), 4);
|
||||
EXPECT_EQ(results[1].total_weight, 10);
|
||||
EXPECT_EQ(GetProp(results[2].vertex), 3);
|
||||
EXPECT_EQ(results[2].total_weight, 13);
|
||||
}
|
||||
{
|
||||
auto results = ExpandWShortest(EdgeAtom::Direction::BOTH, 1000,
|
||||
PropNe(filter_node, 1));
|
||||
ASSERT_EQ(results.size(), 3);
|
||||
EXPECT_EQ(GetProp(results[0].vertex), 2);
|
||||
EXPECT_EQ(results[0].total_weight, 3);
|
||||
EXPECT_EQ(GetProp(results[1].vertex), 3);
|
||||
EXPECT_EQ(results[1].total_weight, 6);
|
||||
EXPECT_EQ(GetProp(results[2].vertex), 4);
|
||||
EXPECT_EQ(results[2].total_weight, 9);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(QueryPlanExpandWeightedShortestPath, GraphState) {
|
||||
auto ExpandSize = [this](GraphView graph_view) {
|
||||
return ExpandWShortest(EdgeAtom::Direction::BOTH, 1000, LITERAL(true),
|
||||
graph_view)
|
||||
.size();
|
||||
};
|
||||
EXPECT_EQ(ExpandSize(GraphView::OLD), 4);
|
||||
EXPECT_EQ(ExpandSize(GraphView::NEW), 4);
|
||||
auto new_vertex = dba.InsertVertex();
|
||||
new_vertex.PropsSet(prop.second, 5);
|
||||
auto edge = dba.InsertEdge(v[4], new_vertex, edge_type);
|
||||
edge.PropsSet(prop.second, 2);
|
||||
EXPECT_EQ(CountIterable(dba.Vertices(false)), 5);
|
||||
EXPECT_EQ(CountIterable(dba.Vertices(true)), 6);
|
||||
EXPECT_EQ(ExpandSize(GraphView::OLD), 4);
|
||||
EXPECT_EQ(ExpandSize(GraphView::NEW), 5);
|
||||
dba.AdvanceCommand();
|
||||
EXPECT_EQ(ExpandSize(GraphView::OLD), 5);
|
||||
EXPECT_EQ(ExpandSize(GraphView::NEW), 5);
|
||||
}
|
||||
|
||||
TEST_F(QueryPlanExpandWeightedShortestPath, ExistingNode) {
|
||||
auto ExpandPreceeding = [this](
|
||||
std::experimental::optional<int> preceeding_node_id) {
|
||||
// scan the nodes optionally filtering on property value
|
||||
auto n0 = MakeScanAll(storage, symbol_table, "n0");
|
||||
if (preceeding_node_id) {
|
||||
auto filter = std::make_shared<Filter>(
|
||||
n0.op_, EQ(PROPERTY_LOOKUP(n0.node_->identifier_, prop),
|
||||
LITERAL(*preceeding_node_id)));
|
||||
// inject the filter op into the ScanAllTuple. that way the filter op
|
||||
// can be passed into the ExpandWShortest function without too much
|
||||
// refactor
|
||||
n0.op_ = filter;
|
||||
}
|
||||
|
||||
return ExpandWShortest(EdgeAtom::Direction::OUT, 1000, LITERAL(true),
|
||||
GraphView::AS_IS, std::experimental::nullopt, &n0);
|
||||
};
|
||||
|
||||
EXPECT_EQ(ExpandPreceeding(std::experimental::nullopt).size(), 20);
|
||||
{
|
||||
auto results = ExpandPreceeding(3);
|
||||
ASSERT_EQ(results.size(), 4);
|
||||
for (int i = 0; i < 4; i++) EXPECT_EQ(GetProp(results[i].vertex), 3);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(QueryPlanExpandWeightedShortestPath, UpperBound) {
|
||||
{
|
||||
auto results = ExpandWShortest(EdgeAtom::Direction::BOTH, 2, LITERAL(true));
|
||||
ASSERT_EQ(results.size(), 4);
|
||||
EXPECT_EQ(GetProp(results[0].vertex), 2);
|
||||
EXPECT_EQ(results[0].total_weight, 3);
|
||||
EXPECT_EQ(GetProp(results[1].vertex), 1);
|
||||
EXPECT_EQ(results[1].total_weight, 5);
|
||||
EXPECT_EQ(GetProp(results[2].vertex), 3);
|
||||
EXPECT_EQ(results[2].total_weight, 6);
|
||||
EXPECT_EQ(GetProp(results[3].vertex), 4);
|
||||
EXPECT_EQ(results[3].total_weight, 10);
|
||||
}
|
||||
{
|
||||
auto results = ExpandWShortest(EdgeAtom::Direction::BOTH, 1, LITERAL(true));
|
||||
ASSERT_EQ(results.size(), 3);
|
||||
EXPECT_EQ(GetProp(results[0].vertex), 2);
|
||||
EXPECT_EQ(results[0].total_weight, 3);
|
||||
EXPECT_EQ(GetProp(results[1].vertex), 1);
|
||||
EXPECT_EQ(results[1].total_weight, 5);
|
||||
EXPECT_EQ(GetProp(results[2].vertex), 4);
|
||||
EXPECT_EQ(results[2].total_weight, 12);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(QueryPlanExpandWeightedShortestPath, Exceptions) {
|
||||
{
|
||||
auto new_vertex = dba.InsertVertex();
|
||||
new_vertex.PropsSet(prop.second, 5);
|
||||
auto edge = dba.InsertEdge(v[4], new_vertex, edge_type);
|
||||
edge.PropsSet(prop.second, "not a number");
|
||||
EXPECT_THROW(ExpandWShortest(EdgeAtom::Direction::BOTH, 1000, LITERAL(true),
|
||||
GraphView::NEW),
|
||||
QueryRuntimeException);
|
||||
}
|
||||
{
|
||||
auto new_vertex = dba.InsertVertex();
|
||||
new_vertex.PropsSet(prop.second, 5);
|
||||
auto edge = dba.InsertEdge(v[4], new_vertex, edge_type);
|
||||
edge.PropsSet(prop.second, -10); // negative weight
|
||||
EXPECT_THROW(ExpandWShortest(EdgeAtom::Direction::BOTH, 1000, LITERAL(true),
|
||||
GraphView::NEW),
|
||||
QueryRuntimeException);
|
||||
}
|
||||
{
|
||||
// negative upper bound
|
||||
EXPECT_THROW(ExpandWShortest(EdgeAtom::Direction::BOTH, -1, LITERAL(true),
|
||||
GraphView::NEW),
|
||||
QueryRuntimeException);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(QueryPlan, ExpandOptional) {
|
||||
database::SingleNode db;
|
||||
database::GraphDbAccessor dba(db);
|
||||
|
Loading…
Reference in New Issue
Block a user