Fix bug on AllShortest with multiple edges between nodes (#832)
This commit is contained in:
parent
029be10f1d
commit
0819b40202
@ -1834,7 +1834,7 @@ class ExpandAllShortestPathsCursor : public query::plan::Cursor {
|
|||||||
: self_(self),
|
: self_(self),
|
||||||
input_cursor_(self_.input_->MakeCursor(mem)),
|
input_cursor_(self_.input_->MakeCursor(mem)),
|
||||||
visited_cost_(mem),
|
visited_cost_(mem),
|
||||||
expanded_(mem),
|
total_cost_(mem),
|
||||||
next_edges_(mem),
|
next_edges_(mem),
|
||||||
traversal_stack_(mem),
|
traversal_stack_(mem),
|
||||||
pq_(mem) {}
|
pq_(mem) {}
|
||||||
@ -1844,6 +1844,9 @@ class ExpandAllShortestPathsCursor : public query::plan::Cursor {
|
|||||||
|
|
||||||
ExpressionEvaluator evaluator(&frame, context.symbol_table, context.evaluation_context, context.db_accessor,
|
ExpressionEvaluator evaluator(&frame, context.symbol_table, context.evaluation_context, context.db_accessor,
|
||||||
storage::View::OLD);
|
storage::View::OLD);
|
||||||
|
auto create_state = [this](const VertexAccessor &vertex, int64_t depth) {
|
||||||
|
return std::make_pair(vertex, upper_bound_set_ ? depth : 0);
|
||||||
|
};
|
||||||
|
|
||||||
// For the given (edge, direction, weight, depth) tuple checks if they
|
// For the given (edge, direction, weight, depth) tuple checks if they
|
||||||
// satisfy the "where" condition. if so, places them in the priority
|
// satisfy the "where" condition. if so, places them in the priority
|
||||||
@ -1935,73 +1938,118 @@ class ExpandAllShortestPathsCursor : public query::plan::Cursor {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check if upper bound exists
|
std::optional<VertexAccessor> start_vertex;
|
||||||
upper_bound_ = self_.upper_bound_
|
auto *memory = context.evaluation_context.memory;
|
||||||
? EvaluateInt(&evaluator, self_.upper_bound_, "Max depth in all shortest paths expansion")
|
|
||||||
: std::numeric_limits<int64_t>::max();
|
auto create_path = [this, &frame, &memory]() {
|
||||||
|
auto ¤t_level = traversal_stack_.back();
|
||||||
|
auto &edges_on_frame = frame[self_.common_.edge_symbol].ValueList();
|
||||||
|
|
||||||
|
// Clean out the current stack
|
||||||
|
if (current_level.empty()) {
|
||||||
|
if (!edges_on_frame.empty()) {
|
||||||
|
if (!self_.is_reverse_)
|
||||||
|
edges_on_frame.erase(edges_on_frame.end());
|
||||||
|
else
|
||||||
|
edges_on_frame.erase(edges_on_frame.begin());
|
||||||
|
}
|
||||||
|
traversal_stack_.pop_back();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto [current_edge, current_edge_direction, current_weight] = current_level.back();
|
||||||
|
current_level.pop_back();
|
||||||
|
|
||||||
|
// Edges order depends on direction of expansion
|
||||||
|
if (!self_.is_reverse_)
|
||||||
|
edges_on_frame.emplace_back(current_edge);
|
||||||
|
else
|
||||||
|
edges_on_frame.emplace(edges_on_frame.begin(), current_edge);
|
||||||
|
|
||||||
|
auto next_vertex = current_edge_direction == EdgeAtom::Direction::IN ? current_edge.From() : current_edge.To();
|
||||||
|
frame[self_.total_weight_.value()] = current_weight;
|
||||||
|
|
||||||
|
if (next_edges_.find({next_vertex, traversal_stack_.size()}) != next_edges_.end()) {
|
||||||
|
auto next_vertex_edges = next_edges_[{next_vertex, traversal_stack_.size()}];
|
||||||
|
traversal_stack_.emplace_back(std::move(next_vertex_edges));
|
||||||
|
} else {
|
||||||
|
// Signal the end of iteration
|
||||||
|
utils::pmr::list<DirectedEdge> empty(memory);
|
||||||
|
traversal_stack_.emplace_back(std::move(empty));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((current_weight > visited_cost_.at(next_vertex)).ValueBool()) return false;
|
||||||
|
|
||||||
|
// Place destination node on the frame, handle existence flag
|
||||||
|
if (self_.common_.existing_node) {
|
||||||
|
const auto &node = frame[self_.common_.node_symbol];
|
||||||
|
ExpectType(self_.common_.node_symbol, node, TypedValue::Type::Vertex);
|
||||||
|
if (node.ValueVertex() != next_vertex) return false;
|
||||||
|
} else {
|
||||||
|
frame[self_.common_.node_symbol] = next_vertex;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
auto create_DFS_traversal_tree = [this, &context, &memory, &create_state, &expand_from_vertex]() {
|
||||||
|
while (!pq_.empty()) {
|
||||||
|
if (MustAbort(context)) throw HintedAbortError();
|
||||||
|
|
||||||
|
const auto [current_weight, current_depth, current_vertex, directed_edge] = pq_.top();
|
||||||
|
pq_.pop();
|
||||||
|
|
||||||
|
const auto &[current_edge, direction, weight] = directed_edge;
|
||||||
|
auto current_state = create_state(current_vertex, current_depth);
|
||||||
|
|
||||||
|
auto position = total_cost_.find(current_state);
|
||||||
|
if (position != total_cost_.end()) {
|
||||||
|
if ((position->second < current_weight).ValueBool()) continue;
|
||||||
|
} else {
|
||||||
|
total_cost_.emplace(current_state, current_weight);
|
||||||
|
if (current_depth < upper_bound_) {
|
||||||
|
expand_from_vertex(current_vertex, current_weight, current_depth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Searching for a previous vertex in the expansion
|
||||||
|
auto prev_vertex = direction == EdgeAtom::Direction::IN ? current_edge.To() : current_edge.From();
|
||||||
|
|
||||||
|
// Update the parent
|
||||||
|
if (next_edges_.find({prev_vertex, current_depth - 1}) == next_edges_.end()) {
|
||||||
|
utils::pmr::list<DirectedEdge> empty(memory);
|
||||||
|
next_edges_[{prev_vertex, current_depth - 1}] = std::move(empty);
|
||||||
|
}
|
||||||
|
next_edges_.at({prev_vertex, current_depth - 1}).emplace_back(directed_edge);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// upper_bound_set is used when storing visited edges, because with an upper bound we also consider suboptimal paths
|
||||||
|
// if they are shorter in depth
|
||||||
|
if (self_.upper_bound_) {
|
||||||
|
upper_bound_ = EvaluateInt(&evaluator, self_.upper_bound_, "Max depth in all shortest path expansion");
|
||||||
|
upper_bound_set_ = true;
|
||||||
|
} else {
|
||||||
|
upper_bound_ = std::numeric_limits<int64_t>::max();
|
||||||
|
upper_bound_set_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if upper bound is valid
|
// Check if upper bound is valid
|
||||||
if (upper_bound_ < 1) {
|
if (upper_bound_ < 1) {
|
||||||
throw QueryRuntimeException("Maximum depth in all shortest paths expansion must be at least 1.");
|
throw QueryRuntimeException("Maximum depth in all shortest paths expansion must be at least 1.");
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<VertexAccessor> start_vertex;
|
// On first Pull run, traversal stack and priority queue are empty, so we start a pulling stream
|
||||||
auto *memory = context.evaluation_context.memory;
|
// and create a DFS traversal tree (main part of algorithm). Then we return the first path
|
||||||
|
// created from the DFS traversal tree (basically a DFS algorithm).
|
||||||
|
// On each subsequent Pull run, paths are created from the traversal stack and returned.
|
||||||
while (true) {
|
while (true) {
|
||||||
// Check if there is an external error.
|
// Check if there is an external error.
|
||||||
if (MustAbort(context)) throw HintedAbortError();
|
if (MustAbort(context)) throw HintedAbortError();
|
||||||
|
|
||||||
// If traversal stack if filled, the DFS traversal tree is created. Traverse the tree iteratively by
|
// The algorithm is run all at once by create_DFS_traversal_tree, after which we
|
||||||
// preserving the traversal state on stack.
|
// traverse the tree iteratively by preserving the traversal state on stack.
|
||||||
while (!traversal_stack_.empty()) {
|
while (!traversal_stack_.empty()) {
|
||||||
auto ¤t_level = traversal_stack_.back();
|
if (create_path()) return true;
|
||||||
auto &edges_on_frame = frame[self_.common_.edge_symbol].ValueList();
|
|
||||||
|
|
||||||
// Clean out the current stack
|
|
||||||
if (current_level.empty()) {
|
|
||||||
if (!edges_on_frame.empty()) {
|
|
||||||
if (!self_.is_reverse_)
|
|
||||||
edges_on_frame.erase(edges_on_frame.end());
|
|
||||||
else
|
|
||||||
edges_on_frame.erase(edges_on_frame.begin());
|
|
||||||
}
|
|
||||||
traversal_stack_.pop_back();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto [current_edge, current_edge_direction, current_weight] = current_level.back();
|
|
||||||
current_level.pop_back();
|
|
||||||
|
|
||||||
// Edges order depends on direction of expansion
|
|
||||||
if (!self_.is_reverse_)
|
|
||||||
edges_on_frame.emplace_back(current_edge);
|
|
||||||
else
|
|
||||||
edges_on_frame.emplace(edges_on_frame.begin(), current_edge);
|
|
||||||
|
|
||||||
auto next_vertex = current_edge_direction == EdgeAtom::Direction::IN ? current_edge.From() : current_edge.To();
|
|
||||||
frame[self_.total_weight_.value()] = current_weight;
|
|
||||||
|
|
||||||
if (next_edges_.find({next_vertex, traversal_stack_.size()}) != next_edges_.end()) {
|
|
||||||
auto next_vertex_edges = next_edges_[{next_vertex, traversal_stack_.size()}];
|
|
||||||
traversal_stack_.emplace_back(std::move(next_vertex_edges));
|
|
||||||
} else {
|
|
||||||
// Signal the end of iteration
|
|
||||||
utils::pmr::list<DirectedEdge> empty(memory);
|
|
||||||
traversal_stack_.emplace_back(std::move(empty));
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((current_weight > visited_cost_.at(next_vertex)).ValueBool()) continue;
|
|
||||||
|
|
||||||
// Place destination node on the frame, handle existence flag
|
|
||||||
if (self_.common_.existing_node) {
|
|
||||||
const auto &node = frame[self_.common_.node_symbol];
|
|
||||||
ExpectType(self_.common_.node_symbol, node, TypedValue::Type::Vertex);
|
|
||||||
if (node.ValueVertex() != next_vertex) continue;
|
|
||||||
} else {
|
|
||||||
frame[self_.common_.node_symbol] = next_vertex;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If priority queue is empty start new pulling stream.
|
// If priority queue is empty start new pulling stream.
|
||||||
@ -2022,9 +2070,9 @@ class ExpandAllShortestPathsCursor : public query::plan::Cursor {
|
|||||||
|
|
||||||
// Clear existing data structures.
|
// Clear existing data structures.
|
||||||
visited_cost_.clear();
|
visited_cost_.clear();
|
||||||
expanded_.clear();
|
|
||||||
next_edges_.clear();
|
next_edges_.clear();
|
||||||
traversal_stack_.clear();
|
traversal_stack_.clear();
|
||||||
|
total_cost_.clear();
|
||||||
|
|
||||||
expand_from_vertex(*start_vertex, TypedValue(), 0);
|
expand_from_vertex(*start_vertex, TypedValue(), 0);
|
||||||
visited_cost_.emplace(*start_vertex, 0);
|
visited_cost_.emplace(*start_vertex, 0);
|
||||||
@ -2032,33 +2080,9 @@ class ExpandAllShortestPathsCursor : public query::plan::Cursor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create a DFS traversal tree from the start node
|
// Create a DFS traversal tree from the start node
|
||||||
while (!pq_.empty()) {
|
create_DFS_traversal_tree();
|
||||||
if (MustAbort(context)) throw HintedAbortError();
|
|
||||||
|
|
||||||
const auto [current_weight, current_depth, current_vertex, directed_edge] = pq_.top();
|
|
||||||
pq_.pop();
|
|
||||||
|
|
||||||
const auto &[current_edge, direction, weight] = directed_edge;
|
|
||||||
if (expanded_.contains(current_edge)) continue;
|
|
||||||
expanded_.emplace(current_edge);
|
|
||||||
|
|
||||||
// Expand only if what we've just expanded is less than max depth.
|
|
||||||
if (current_depth < upper_bound_) {
|
|
||||||
expand_from_vertex(current_vertex, current_weight, current_depth);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Searching for a previous vertex in the expansion
|
|
||||||
auto prev_vertex = direction == EdgeAtom::Direction::IN ? current_edge.To() : current_edge.From();
|
|
||||||
|
|
||||||
// Update the parent
|
|
||||||
if (next_edges_.find({prev_vertex, current_depth - 1}) == next_edges_.end()) {
|
|
||||||
utils::pmr::list<DirectedEdge> empty(memory);
|
|
||||||
next_edges_[{prev_vertex, current_depth - 1}] = std::move(empty);
|
|
||||||
}
|
|
||||||
|
|
||||||
next_edges_.at({prev_vertex, current_depth - 1}).emplace_back(directed_edge);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// DFS traversal tree is create,
|
||||||
if (start_vertex && next_edges_.find({*start_vertex, 0}) != next_edges_.end()) {
|
if (start_vertex && next_edges_.find({*start_vertex, 0}) != next_edges_.end()) {
|
||||||
auto start_vertex_edges = next_edges_[{*start_vertex, 0}];
|
auto start_vertex_edges = next_edges_[{*start_vertex, 0}];
|
||||||
traversal_stack_.emplace_back(std::move(start_vertex_edges));
|
traversal_stack_.emplace_back(std::move(start_vertex_edges));
|
||||||
@ -2071,9 +2095,9 @@ class ExpandAllShortestPathsCursor : public query::plan::Cursor {
|
|||||||
void Reset() override {
|
void Reset() override {
|
||||||
input_cursor_->Reset();
|
input_cursor_->Reset();
|
||||||
visited_cost_.clear();
|
visited_cost_.clear();
|
||||||
expanded_.clear();
|
|
||||||
next_edges_.clear();
|
next_edges_.clear();
|
||||||
traversal_stack_.clear();
|
traversal_stack_.clear();
|
||||||
|
total_cost_.clear();
|
||||||
ClearQueue();
|
ClearQueue();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2083,6 +2107,7 @@ class ExpandAllShortestPathsCursor : public query::plan::Cursor {
|
|||||||
|
|
||||||
// Upper bound on the path length.
|
// Upper bound on the path length.
|
||||||
int64_t upper_bound_{-1};
|
int64_t upper_bound_{-1};
|
||||||
|
bool upper_bound_set_{false};
|
||||||
|
|
||||||
struct AspStateHash {
|
struct AspStateHash {
|
||||||
size_t operator()(const std::pair<VertexAccessor, int64_t> &key) const {
|
size_t operator()(const std::pair<VertexAccessor, int64_t> &key) const {
|
||||||
@ -2094,8 +2119,8 @@ class ExpandAllShortestPathsCursor : public query::plan::Cursor {
|
|||||||
using NextEdgesState = std::pair<VertexAccessor, int64_t>;
|
using NextEdgesState = std::pair<VertexAccessor, int64_t>;
|
||||||
// Maps vertices to minimum weights they got in expansion.
|
// Maps vertices to minimum weights they got in expansion.
|
||||||
utils::pmr::unordered_map<VertexAccessor, TypedValue> visited_cost_;
|
utils::pmr::unordered_map<VertexAccessor, TypedValue> visited_cost_;
|
||||||
// Marking the expanded edges to prevent multiple visits.
|
// Maps vertices to weights they got in expansion.
|
||||||
utils::pmr::unordered_set<EdgeAccessor> expanded_;
|
utils::pmr::unordered_map<NextEdgesState, TypedValue, AspStateHash> total_cost_;
|
||||||
// Maps the vertex with the potential expansion edge.
|
// Maps the vertex with the potential expansion edge.
|
||||||
utils::pmr::unordered_map<NextEdgesState, utils::pmr::list<DirectedEdge>, AspStateHash> next_edges_;
|
utils::pmr::unordered_map<NextEdgesState, utils::pmr::list<DirectedEdge>, AspStateHash> next_edges_;
|
||||||
// Stack indicating the traversal level.
|
// Stack indicating the traversal level.
|
||||||
|
@ -1,191 +1,205 @@
|
|||||||
Feature: All Shortest Path
|
Feature: All Shortest Path
|
||||||
|
|
||||||
Scenario: Test match allShortest upper bound
|
Scenario: Test match allShortest upper bound
|
||||||
Given an empty graph
|
Given an empty graph
|
||||||
And having executed:
|
And having executed:
|
||||||
"""
|
"""
|
||||||
CREATE (n {a:'0'})-[:r {w: 1}]->({a:'1'})-[:r {w: 1}]->({a:'2'}), (n)-[:r {w: 1}]->({a:'3'})
|
CREATE (n {a:'0'})-[:r {w: 1}]->({a:'1'})-[:r {w: 1}]->({a:'2'}), (n)-[:r {w: 1}]->({a:'3'})
|
||||||
"""
|
"""
|
||||||
When executing query:
|
When executing query:
|
||||||
"""
|
"""
|
||||||
MATCH (n {a:'0'})-[le *allShortest 1 (e, n | e.w ) w]->(m) RETURN m.a
|
MATCH (n {a:'0'})-[le *allShortest 1 (e, n | e.w ) w]->(m) RETURN m.a
|
||||||
"""
|
"""
|
||||||
Then the result should be:
|
Then the result should be:
|
||||||
| m.a |
|
| m.a |
|
||||||
| '1' |
|
| '1' |
|
||||||
| '3' |
|
| '3' |
|
||||||
|
|
||||||
Scenario: Test match allShortest upper bound 2
|
Scenario: Test match allShortest upper bound 2
|
||||||
Given an empty graph
|
Given an empty graph
|
||||||
And having executed:
|
And having executed:
|
||||||
"""
|
"""
|
||||||
CREATE (a {a:'0'})-[:r {w: 2}]->(b {a:'1'})-[:r {w: 3}]->(c {a:'2'}),
|
CREATE (a {a:'0'})-[:r {w: 2}]->(b {a:'1'})-[:r {w: 3}]->(c {a:'2'}),
|
||||||
(a)-[:re {w: 2}]->(b),
|
(a)-[:re {w: 2}]->(b),
|
||||||
(b)-[:re {w:3}]->(c),
|
(b)-[:re {w:3}]->(c),
|
||||||
({a: '4'})<-[:r {w: 1}]-(a),
|
({a: '4'})<-[:r {w: 1}]-(a),
|
||||||
({a: '5'})<-[:r {w: 1}]-(a),
|
({a: '5'})<-[:r {w: 1}]-(a),
|
||||||
(c)-[:r {w: 1}]->({a: '6'}),
|
(c)-[:r {w: 1}]->({a: '6'}),
|
||||||
(c)-[:r {w: 1}]->({a: '7'})
|
(c)-[:r {w: 1}]->({a: '7'})
|
||||||
"""
|
"""
|
||||||
When executing query:
|
When executing query:
|
||||||
"""
|
"""
|
||||||
MATCH path=(n {a:'0'})-[r *allShortest ..2 (e, n | 1 ) w]->(m {a:'2'}) RETURN COUNT(path) AS c
|
MATCH path=(n {a:'0'})-[r *allShortest ..2 (e, n | 1 ) w]->(m {a:'2'}) RETURN COUNT(path) AS c
|
||||||
"""
|
"""
|
||||||
Then the result should be:
|
Then the result should be:
|
||||||
| c |
|
| c |
|
||||||
| 4 |
|
| 4 |
|
||||||
|
|
||||||
Scenario: Test match allShortest filtered
|
Scenario: Test match allShortest filtered
|
||||||
Given an empty graph
|
Given an empty graph
|
||||||
And having executed:
|
And having executed:
|
||||||
"""
|
"""
|
||||||
CREATE (n {a:'0'})-[:r {w: 1}]->({a:'1'})-[:r {w: 1}]->({a:'2'}), (n)-[:r {w: 1}]->({a:'3'})
|
CREATE (n {a:'0'})-[:r {w: 1}]->({a:'1'})-[:r {w: 1}]->({a:'2'}), (n)-[:r {w: 1}]->({a:'3'})
|
||||||
"""
|
"""
|
||||||
When executing query:
|
When executing query:
|
||||||
"""
|
"""
|
||||||
MATCH (n {a:'0'})-[le *allShortest 1 (e, n | e.w ) w (e, n | n.a = '3')]->(m) RETURN m.a
|
MATCH (n {a:'0'})-[le *allShortest 1 (e, n | e.w ) w (e, n | n.a = '3')]->(m) RETURN m.a
|
||||||
"""
|
"""
|
||||||
Then the result should be:
|
Then the result should be:
|
||||||
| m.a |
|
| m.a |
|
||||||
| '3' |
|
| '3' |
|
||||||
|
|
||||||
Scenario: Test match allShortest resulting edge list
|
Scenario: Test match allShortest resulting edge list
|
||||||
Given an empty graph
|
Given an empty graph
|
||||||
And having executed:
|
And having executed:
|
||||||
"""
|
"""
|
||||||
CREATE (n {a:'0'})-[:r {w: 1}]->({a:'1'})-[:r {w: 2}]->({a:'2'}), (n)-[:r {w: 4}]->({a:'3'})
|
CREATE (n {a:'0'})-[:r {w: 1}]->({a:'1'})-[:r {w: 2}]->({a:'2'}), (n)-[:r {w: 4}]->({a:'3'})
|
||||||
"""
|
"""
|
||||||
When executing query:
|
When executing query:
|
||||||
"""
|
"""
|
||||||
MATCH (n {a:'0'})-[le *allShortest 10 (e, n | e.w ) w]->(m) RETURN m.a, size(le) as s, w
|
MATCH (n {a:'0'})-[le *allShortest 10 (e, n | e.w ) w]->(m) RETURN m.a, size(le) as s, w
|
||||||
"""
|
"""
|
||||||
Then the result should be:
|
Then the result should be:
|
||||||
| m.a | s | w |
|
| m.a | s | w |
|
||||||
| '1' | 1 | 1 |
|
| '1' | 1 | 1 |
|
||||||
| '2' | 2 | 3 |
|
| '2' | 2 | 3 |
|
||||||
| '3' | 1 | 4 |
|
| '3' | 1 | 4 |
|
||||||
|
|
||||||
Scenario: Test match allShortest single edge type filtered
|
Scenario: Test match allShortest single edge type filtered
|
||||||
Given an empty graph
|
Given an empty graph
|
||||||
And having executed:
|
And having executed:
|
||||||
"""
|
"""
|
||||||
CREATE (n {a:'0'})-[:r0 {w: 1}]->({a:'1'})-[:r {w: 2}]->({a:'2'}), (n)-[:r {w: 3}]->({a:'4'})
|
CREATE (n {a:'0'})-[:r0 {w: 1}]->({a:'1'})-[:r {w: 2}]->({a:'2'}), (n)-[:r {w: 3}]->({a:'4'})
|
||||||
"""
|
"""
|
||||||
When executing query:
|
When executing query:
|
||||||
"""
|
"""
|
||||||
MATCH ()-[le:r0 *allShortest 10 (e, n | e.w) w]->(m)
|
MATCH ()-[le:r0 *allShortest 10 (e, n | e.w) w]->(m)
|
||||||
RETURN size(le) AS s, m.a
|
RETURN size(le) AS s, m.a
|
||||||
"""
|
"""
|
||||||
Then the result should be:
|
Then the result should be:
|
||||||
| s | m.a |
|
| s | m.a |
|
||||||
| 1 | '1' |
|
| 1 | '1' |
|
||||||
|
|
||||||
Scenario: Test match allShortest multiple edge types filtered
|
Scenario: Test match allShortest multiple edge types filtered
|
||||||
Given an empty graph
|
Given an empty graph
|
||||||
And having executed:
|
And having executed:
|
||||||
"""
|
"""
|
||||||
CREATE (n {a:'0'})-[:r0 {w: 1}]->({a:'1'})-[:r1 {w: 2}]->({a:'2'}), (n)-[:r {w: 3}]->({a:'4'})
|
CREATE (n {a:'0'})-[:r0 {w: 1}]->({a:'1'})-[:r1 {w: 2}]->({a:'2'}), (n)-[:r {w: 3}]->({a:'4'})
|
||||||
"""
|
"""
|
||||||
When executing query:
|
When executing query:
|
||||||
"""
|
"""
|
||||||
MATCH ()-[le :r0|:r1 *allShortest 10 (e, n | e.w) w]->(m) WHERE size(le) > 1
|
MATCH ()-[le :r0|:r1 *allShortest 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
|
RETURN size(le) AS s, (le[0]).w AS r0, (le[1]).w AS r1
|
||||||
"""
|
"""
|
||||||
Then the result should be:
|
Then the result should be:
|
||||||
| s | r0 | r1 |
|
| s | r0 | r1 |
|
||||||
| 2 | 1 | 2 |
|
| 2 | 1 | 2 |
|
||||||
|
|
||||||
Scenario: Test match allShortest property filters
|
Scenario: Test match allShortest property filters
|
||||||
Given an empty graph
|
Given an empty graph
|
||||||
And having executed:
|
And having executed:
|
||||||
"""
|
"""
|
||||||
CREATE (n {a:'0'})-[:r {w: 1}]->({a:'1'})-[:r {w: 2}]->({a:'2'}), (n)-[:r {w: 3}]->({a:'4'})
|
CREATE (n {a:'0'})-[:r {w: 1}]->({a:'1'})-[:r {w: 2}]->({a:'2'}), (n)-[:r {w: 3}]->({a:'4'})
|
||||||
"""
|
"""
|
||||||
When executing query:
|
When executing query:
|
||||||
"""
|
"""
|
||||||
MATCH ()-[le *allShortest 10 {w:1} (e, n | e.w ) total_weight]->(m)
|
MATCH ()-[le *allShortest 10 {w:1} (e, n | e.w ) total_weight]->(m)
|
||||||
RETURN size(le) AS s, (le[0]).w AS r0
|
RETURN size(le) AS s, (le[0]).w AS r0
|
||||||
"""
|
"""
|
||||||
Then the result should be:
|
Then the result should be:
|
||||||
| s | r0 |
|
| s | r0 |
|
||||||
| 1 | 1 |
|
| 1 | 1 |
|
||||||
|
|
||||||
Scenario: Test match allShortest weight not a number
|
Scenario: Test match allShortest weight not a number
|
||||||
Given an empty graph
|
Given an empty graph
|
||||||
And having executed:
|
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'})
|
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:
|
When executing query:
|
||||||
"""
|
"""
|
||||||
MATCH ()-[le *allShortest 10 (e, n | e.w ) total_weight]->(m)
|
MATCH ()-[le *allShortest 10 (e, n | e.w ) total_weight]->(m)
|
||||||
RETURN le, total_weight
|
RETURN le, total_weight
|
||||||
"""
|
"""
|
||||||
Then an error should be raised
|
Then an error should be raised
|
||||||
|
|
||||||
Scenario: Test match allShortest negative weight
|
Scenario: Test match allShortest negative weight
|
||||||
Given an empty graph
|
Given an empty graph
|
||||||
And having executed:
|
And having executed:
|
||||||
"""
|
"""
|
||||||
CREATE (n {a:'0'})-[:r {w: -1}]->({a:'1'})-[:r {w: 2}]->({a:'2'}), (n)-[:r {w: 3}]->({a:'4'})
|
CREATE (n {a:'0'})-[:r {w: -1}]->({a:'1'})-[:r {w: 2}]->({a:'2'}), (n)-[:r {w: 3}]->({a:'4'})
|
||||||
"""
|
"""
|
||||||
When executing query:
|
When executing query:
|
||||||
"""
|
"""
|
||||||
MATCH ()-[le *allShortest 10 (e, n | e.w ) total_weight]->(m)
|
MATCH ()-[le *allShortest 10 (e, n | e.w ) total_weight]->(m)
|
||||||
RETURN le, total_weight
|
RETURN le, total_weight
|
||||||
"""
|
"""
|
||||||
Then an error should be raised
|
Then an error should be raised
|
||||||
|
|
||||||
Scenario: Test match allShortest weight duration
|
Scenario: Test match allShortest weight duration
|
||||||
Given an empty graph
|
Given an empty graph
|
||||||
And having executed:
|
And having executed:
|
||||||
"""
|
"""
|
||||||
CREATE (n {a:'0'})-[:r {w: DURATION('PT1S')}]->({a:'1'})-[:r {w: DURATION('PT2S')}]->({a:'2'}), (n)-[:r {w: DURATION('PT4S')}]->({a:'3'})
|
CREATE (n {a:'0'})-[:r {w: DURATION('PT1S')}]->({a:'1'})-[:r {w: DURATION('PT2S')}]->({a:'2'}), (n)-[:r {w: DURATION('PT4S')}]->({a:'3'})
|
||||||
"""
|
"""
|
||||||
When executing query:
|
When executing query:
|
||||||
"""
|
"""
|
||||||
MATCH (n {a:'0'})-[le *allShortest 10 (e, n | e.w ) w]->(m) RETURN m.a, size(le) as s, w
|
MATCH (n {a:'0'})-[le *allShortest 10 (e, n | e.w ) w]->(m) RETURN m.a, size(le) as s, w
|
||||||
"""
|
"""
|
||||||
Then the result should be:
|
Then the result should be:
|
||||||
| m.a | s | w |
|
| m.a | s | w |
|
||||||
| '1' | 1 | PT1S |
|
| '1' | 1 | PT1S |
|
||||||
| '2' | 2 | PT3S |
|
| '2' | 2 | PT3S |
|
||||||
| '3' | 1 | PT4S |
|
| '3' | 1 | PT4S |
|
||||||
|
|
||||||
Scenario: Test match allShortest weight negative duration
|
Scenario: Test match allShortest weight negative duration
|
||||||
Given an empty graph
|
Given an empty graph
|
||||||
And having executed:
|
And having executed:
|
||||||
"""
|
"""
|
||||||
CREATE (n {a:'0'})-[:r {w: DURATION({seconds: -1})}]->({a:'1'})-[:r {w: DURATION('PT2S')}]->({a:'2'}), (n)-[:r {w: DURATION('PT4S')}]->({a:'3'})
|
CREATE (n {a:'0'})-[:r {w: DURATION({seconds: -1})}]->({a:'1'})-[:r {w: DURATION('PT2S')}]->({a:'2'}), (n)-[:r {w: DURATION('PT4S')}]->({a:'3'})
|
||||||
"""
|
"""
|
||||||
When executing query:
|
When executing query:
|
||||||
"""
|
"""
|
||||||
MATCH (n {a:'0'})-[le *allShortest 10 (e, n | e.w ) w]->(m) RETURN m.a, size(le) as s, w
|
MATCH (n {a:'0'})-[le *allShortest 10 (e, n | e.w ) w]->(m) RETURN m.a, size(le) as s, w
|
||||||
"""
|
"""
|
||||||
Then an error should be raised
|
Then an error should be raised
|
||||||
|
|
||||||
Scenario: Test match allShortest weight mixed numeric and duration as weights
|
Scenario: Test match allShortest weight mixed numeric and duration as weights
|
||||||
Given an empty graph
|
Given an empty graph
|
||||||
And having executed:
|
And having executed:
|
||||||
"""
|
"""
|
||||||
CREATE (n {a:'0'})-[:r {w: 2}]->({a:'1'})-[:r {w: DURATION('PT2S')}]->({a:'2'}), (n)-[:r {w: DURATION('PT4S')}]->({a:'3'})
|
CREATE (n {a:'0'})-[:r {w: 2}]->({a:'1'})-[:r {w: DURATION('PT2S')}]->({a:'2'}), (n)-[:r {w: DURATION('PT4S')}]->({a:'3'})
|
||||||
"""
|
"""
|
||||||
When executing query:
|
When executing query:
|
||||||
"""
|
"""
|
||||||
MATCH (n {a:'0'})-[le *allShortest 10 (e, n | e.w ) w]->(m) RETURN m.a, size(le) as s, w
|
MATCH (n {a:'0'})-[le *allShortest 10 (e, n | e.w ) w]->(m) RETURN m.a, size(le) as s, w
|
||||||
"""
|
"""
|
||||||
Then an error should be raised
|
Then an error should be raised
|
||||||
|
|
||||||
Scenario: Test allShortest return both paths of same length
|
Scenario: Test allShortest return both paths of same length
|
||||||
Given an empty graph
|
Given an empty graph
|
||||||
And having executed:
|
And having executed:
|
||||||
"""
|
"""
|
||||||
CREATE (n {a:'0'})-[:r {w: 2}]->({a:'1'})-[:r {w: 3}]->({a:'2'}), (n)-[:r {w: 5}]->({a:'2'})
|
CREATE (n {a:'0'})-[:r {w: 2}]->({a:'1'})-[:r {w: 3}]->({a:'2'}), (n)-[:r {w: 5}]->({a:'2'})
|
||||||
"""
|
"""
|
||||||
When executing query:
|
When executing query:
|
||||||
"""
|
"""
|
||||||
MATCH path=(n {a:'0'})-[r *allShortest (e, n | e.w ) w]->(m {a:'2'}) RETURN COUNT(path);
|
MATCH path=(n {a:'0'})-[r *allShortest (e, n | e.w ) w]->(m {a:'2'}) RETURN COUNT(path);
|
||||||
"""
|
"""
|
||||||
Then the result should be:
|
Then the result should be:
|
||||||
| COUNT(path) |
|
| COUNT(path) |
|
||||||
| 2 |
|
| 2 |
|
||||||
|
|
||||||
|
Scenario: Test allShortest on different edge between two nodes
|
||||||
|
Given an empty graph
|
||||||
|
And having executed:
|
||||||
|
"""
|
||||||
|
CREATE (n:One), (o:Two), (m:Three), (n)-[:TYPE {cost: 0.3}]->(o), (o)-[:TYPE {cost: 40}]->(m), (o)-[:TYPE {cost: 20}]->(m)
|
||||||
|
"""
|
||||||
|
When executing query:
|
||||||
|
"""
|
||||||
|
MATCH p=(h:One)-[r*allshortest ..5 (e, v | e.cost) total_cost]->(k:Three) return total_cost;
|
||||||
|
"""
|
||||||
|
Then the result should be:
|
||||||
|
| total_cost |
|
||||||
|
| 20.3 |
|
||||||
|
Loading…
Reference in New Issue
Block a user