Query::Plan::VariableExpand added
Summary: Variable expansion logical operator added. Some functionalities are missing: - taking into account optional matching when expanding into existing symbol - accepting Expression bounds (current implementation takes size_t) Also, a TODO is added for handling optional matching in the uniqueness operator (with an Asana task) All this will be done in the following diff, this is already substantial. Also, please consider if we want to have all those `VLOG`s in the code. Not very pretty. And I think that `VLOG` is not compiled-away in release build, will put an asana task. Reviewers: teon.banek, mislav.bradac, buda Reviewed By: teon.banek Subscribers: pullbot Differential Revision: https://phabricator.memgraph.io/D569
This commit is contained in:
parent
b4d2d1ff81
commit
7328f5ec7f
src
tests/unit
@ -376,11 +376,11 @@ std::unique_ptr<Cursor> ScanAllByLabelPropertyValue::MakeCursor(
|
||||
return std::make_unique<ScanAllByLabelPropertyValueCursor>(*this, db);
|
||||
}
|
||||
|
||||
Expand::Expand(Symbol node_symbol, Symbol edge_symbol,
|
||||
EdgeAtom::Direction direction,
|
||||
const std::shared_ptr<LogicalOperator> &input,
|
||||
Symbol input_symbol, bool existing_node, bool existing_edge,
|
||||
GraphView graph_view)
|
||||
ExpandCommon::ExpandCommon(Symbol node_symbol, Symbol edge_symbol,
|
||||
EdgeAtom::Direction direction,
|
||||
const std::shared_ptr<LogicalOperator> &input,
|
||||
Symbol input_symbol, bool existing_node,
|
||||
bool existing_edge, GraphView graph_view)
|
||||
: node_symbol_(node_symbol),
|
||||
edge_symbol_(edge_symbol),
|
||||
direction_(direction),
|
||||
@ -390,6 +390,20 @@ Expand::Expand(Symbol node_symbol, Symbol edge_symbol,
|
||||
existing_edge_(existing_edge),
|
||||
graph_view_(graph_view) {}
|
||||
|
||||
bool ExpandCommon::HandleExistingNode(const VertexAccessor &new_node,
|
||||
Frame &frame) const {
|
||||
if (existing_node_) {
|
||||
TypedValue &old_node_value = frame[node_symbol_];
|
||||
// old_node_value may be Null when using optional matching
|
||||
if (old_node_value.IsNull()) return false;
|
||||
ExpectType(node_symbol_, old_node_value, TypedValue::Type::Vertex);
|
||||
return old_node_value.Value<VertexAccessor>() == new_node;
|
||||
} else {
|
||||
frame[node_symbol_] = new_node;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
ACCEPT_WITH_INPUT(Expand)
|
||||
|
||||
std::unique_ptr<Cursor> Expand::MakeCursor(GraphDbAccessor &db) {
|
||||
@ -405,8 +419,8 @@ bool Expand::ExpandCursor::Pull(Frame &frame, const SymbolTable &symbol_table) {
|
||||
// attempt to get a value from the incoming edges
|
||||
if (in_edges_ && *in_edges_it_ != in_edges_->end()) {
|
||||
EdgeAccessor edge = *(*in_edges_it_)++;
|
||||
if (HandleExistingEdge(edge, frame, symbol_table) &&
|
||||
PullNode(edge, EdgeAtom::Direction::IN, frame, symbol_table))
|
||||
if (HandleExistingEdge(edge, frame) &&
|
||||
PullNode(edge, EdgeAtom::Direction::IN, frame))
|
||||
return true;
|
||||
else
|
||||
continue;
|
||||
@ -420,8 +434,8 @@ bool Expand::ExpandCursor::Pull(Frame &frame, const SymbolTable &symbol_table) {
|
||||
// already done in the block above
|
||||
if (self_.direction_ == EdgeAtom::Direction::BOTH && edge.is_cycle())
|
||||
continue;
|
||||
if (HandleExistingEdge(edge, frame, symbol_table) &&
|
||||
PullNode(edge, EdgeAtom::Direction::OUT, frame, symbol_table))
|
||||
if (HandleExistingEdge(edge, frame) &&
|
||||
PullNode(edge, EdgeAtom::Direction::OUT, frame))
|
||||
return true;
|
||||
else
|
||||
continue;
|
||||
@ -487,9 +501,36 @@ bool Expand::ExpandCursor::InitEdges(Frame &frame,
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Expand::ExpandCursor::PullNode(const EdgeAccessor &new_edge,
|
||||
EdgeAtom::Direction direction,
|
||||
Frame &frame) {
|
||||
switch (direction) {
|
||||
case EdgeAtom::Direction::IN:
|
||||
return self_.HandleExistingNode(new_edge.from(), frame);
|
||||
case EdgeAtom::Direction::OUT:
|
||||
return self_.HandleExistingNode(new_edge.to(), frame);
|
||||
case EdgeAtom::Direction::BOTH:
|
||||
permanent_fail("Must indicate exact expansion direction here");
|
||||
}
|
||||
}
|
||||
|
||||
ExpandVariable::ExpandVariable(Symbol node_symbol, Symbol edge_symbol,
|
||||
EdgeAtom::Direction direction,
|
||||
std::experimental::optional<size_t> lower_bound,
|
||||
std::experimental::optional<size_t> upper_bound,
|
||||
const std::shared_ptr<LogicalOperator> &input,
|
||||
Symbol input_symbol, bool existing_node,
|
||||
bool existing_edge, GraphView graph_view)
|
||||
: ExpandCommon(node_symbol, edge_symbol, direction, input, input_symbol,
|
||||
existing_node, existing_edge, graph_view),
|
||||
lower_bound_(lower_bound),
|
||||
upper_bound_(upper_bound) {
|
||||
debug_assert(lower_bound || upper_bound,
|
||||
"At least one variable-length expansion must be specified");
|
||||
}
|
||||
|
||||
bool Expand::ExpandCursor::HandleExistingEdge(const EdgeAccessor &new_edge,
|
||||
Frame &frame,
|
||||
const SymbolTable &symbol_table) {
|
||||
Frame &frame) const {
|
||||
if (self_.existing_edge_) {
|
||||
TypedValue &old_edge_value = frame[self_.edge_symbol_];
|
||||
// old_edge_value may be Null when using optional matching
|
||||
@ -497,39 +538,276 @@ bool Expand::ExpandCursor::HandleExistingEdge(const EdgeAccessor &new_edge,
|
||||
ExpectType(self_.edge_symbol_, old_edge_value, TypedValue::Type::Edge);
|
||||
return old_edge_value.Value<EdgeAccessor>() == new_edge;
|
||||
} else {
|
||||
// not matching existing, so put the new_edge into the frame and return true
|
||||
frame[self_.edge_symbol_] = new_edge;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
bool Expand::ExpandCursor::PullNode(const EdgeAccessor &new_edge,
|
||||
EdgeAtom::Direction direction, Frame &frame,
|
||||
const SymbolTable &symbol_table) {
|
||||
switch (direction) {
|
||||
case EdgeAtom::Direction::IN:
|
||||
return HandleExistingNode(new_edge.from(), frame, symbol_table);
|
||||
case EdgeAtom::Direction::OUT:
|
||||
return HandleExistingNode(new_edge.to(), frame, symbol_table);
|
||||
case EdgeAtom::Direction::BOTH:
|
||||
permanent_fail("Must indicate exact expansion direction here");
|
||||
}
|
||||
}
|
||||
ACCEPT_WITH_INPUT(ExpandVariable)
|
||||
|
||||
namespace {
|
||||
/**
|
||||
* Helper function that returns an iterable over
|
||||
* <EdgeAtom::Direction, EdgeAccessor> pairs
|
||||
* for the given params.
|
||||
*
|
||||
* @param vertex - The vertex to expand from.
|
||||
* @param direction - Expansion direction. All directions (IN, OUT, BOTH)
|
||||
* are supported.
|
||||
* @return See above.
|
||||
*/
|
||||
auto ExpandFromVertex(const VertexAccessor &vertex,
|
||||
EdgeAtom::Direction direction) {
|
||||
// wraps an EdgeAccessor into a pair <accessor, direction>
|
||||
auto wrapper = [](EdgeAtom::Direction direction, auto &&vertices) {
|
||||
return iter::imap(
|
||||
[direction](const EdgeAccessor &edge) {
|
||||
return std::make_pair(edge, direction);
|
||||
},
|
||||
std::move(vertices));
|
||||
};
|
||||
|
||||
// prepare a vector of elements we'll pass to the itertools
|
||||
std::vector<decltype(wrapper(direction, vertex.in()))> chain_elements;
|
||||
|
||||
if (direction != EdgeAtom::Direction::OUT && vertex.in_degree() > 0)
|
||||
chain_elements.emplace_back(wrapper(EdgeAtom::Direction::IN, vertex.in()));
|
||||
if (direction != EdgeAtom::Direction::IN && vertex.out_degree() > 0)
|
||||
chain_elements.emplace_back(
|
||||
wrapper(EdgeAtom::Direction::OUT, vertex.out()));
|
||||
|
||||
return iter::chain.from_iterable(std::move(chain_elements));
|
||||
}
|
||||
} // annonymous namespace
|
||||
|
||||
class ExpandVariableCursor : public Cursor {
|
||||
public:
|
||||
ExpandVariableCursor(const ExpandVariable &self, GraphDbAccessor &db)
|
||||
: self_(self), input_cursor_(self.input_->MakeCursor(db)) {}
|
||||
|
||||
bool Pull(Frame &frame, const SymbolTable &symbol_table) override {
|
||||
while (true) {
|
||||
if (Expand(frame)) return true;
|
||||
|
||||
if (PullInput(frame, symbol_table)) {
|
||||
// if lower bound is zero we also yield empty paths
|
||||
if (self_.lower_bound_ && self_.lower_bound_.value() == 0) {
|
||||
// take into account existing_edge when yielding empty paths
|
||||
auto &edges_on_frame =
|
||||
frame[self_.edge_symbol_].Value<std::vector<TypedValue>>();
|
||||
if (!self_.existing_edge_ || edges_on_frame.empty()) return true;
|
||||
}
|
||||
// if lower bound is not zero, we just continue, the next
|
||||
// loop iteration will attempt to expand and we're good
|
||||
} else
|
||||
return false;
|
||||
// else continue with the loop, try to expand again
|
||||
// because we succesfully pulled from the input
|
||||
}
|
||||
}
|
||||
|
||||
void Reset() override {
|
||||
input_cursor_->Reset();
|
||||
edges_.clear();
|
||||
edges_it_.clear();
|
||||
}
|
||||
|
||||
private:
|
||||
const ExpandVariable &self_;
|
||||
const std::unique_ptr<Cursor> input_cursor_;
|
||||
|
||||
// a stack of edge iterables corresponding to the level/depth of
|
||||
// the expansion currently being Pulled
|
||||
std::vector<decltype(ExpandFromVertex(std::declval<VertexAccessor>(),
|
||||
EdgeAtom::Direction::IN))>
|
||||
edges_;
|
||||
|
||||
// an iterator indicating the possition in the corresponding edges_ element
|
||||
std::vector<decltype(edges_.begin()->begin())> edges_it_;
|
||||
|
||||
/**
|
||||
* Helper function that Pulls from the input vertex and
|
||||
* makes iteration over it's edges possible.
|
||||
*
|
||||
* @return If the Pull succeeded. If not, this VariableExpandCursor
|
||||
* is exhausted.
|
||||
*/
|
||||
bool PullInput(Frame &frame, const SymbolTable &symbol_table) {
|
||||
if (!input_cursor_->Pull(frame, symbol_table)) return false;
|
||||
|
||||
TypedValue &vertex_value = frame[self_.input_symbol_];
|
||||
// Vertex could be null if it is created by a failed optional match, in
|
||||
// such a case we should stop expanding.
|
||||
if (vertex_value.IsNull()) return false;
|
||||
ExpectType(self_.input_symbol_, vertex_value, TypedValue::Type::Vertex);
|
||||
auto &vertex = vertex_value.Value<VertexAccessor>();
|
||||
switch (self_.graph_view_) {
|
||||
case GraphView::NEW:
|
||||
vertex.SwitchNew();
|
||||
break;
|
||||
case GraphView::OLD:
|
||||
vertex.SwitchOld();
|
||||
break;
|
||||
case GraphView::AS_IS:
|
||||
break;
|
||||
}
|
||||
|
||||
if (!self_.upper_bound_ || self_.upper_bound_.value() > 0) {
|
||||
edges_.emplace_back(ExpandFromVertex(vertex, self_.direction_));
|
||||
edges_it_.emplace_back(edges_.back().begin());
|
||||
}
|
||||
|
||||
// reset the frame value to an empty edge list
|
||||
if (!self_.existing_edge_)
|
||||
frame[self_.edge_symbol_] = std::vector<TypedValue>();
|
||||
|
||||
bool Expand::ExpandCursor::HandleExistingNode(const VertexAccessor new_node,
|
||||
Frame &frame,
|
||||
const SymbolTable &symbol_table) {
|
||||
if (self_.existing_node_) {
|
||||
TypedValue &old_node_value = frame[self_.node_symbol_];
|
||||
// old_node_value may be Null when using optional matching
|
||||
if (old_node_value.IsNull()) return false;
|
||||
ExpectType(self_.node_symbol_, old_node_value, TypedValue::Type::Vertex);
|
||||
return old_node_value.Value<VertexAccessor>() == new_node;
|
||||
} else {
|
||||
// not matching existing, so put the new_node into the frame and return true
|
||||
frame[self_.node_symbol_] = new_node;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enum that indicates what happened during attempted edge
|
||||
* existence and placement check
|
||||
* YIELD - indicates that either existence matched fully
|
||||
* or existence is disabled
|
||||
* PREFIX - indicates that existence is enabled and the current
|
||||
* edge-list is a prefix of the existing one, so expansion
|
||||
* should continue but this particular edge-list should not
|
||||
* be yielded as a valid result
|
||||
* MISMATCH - indicates that existence is enabled and the
|
||||
* current edge-list is not a match, this whole expansion
|
||||
* branch should be abandoned
|
||||
*/
|
||||
enum class EdgePlacementResult { YIELD, PREFIX, MISMATCH };
|
||||
|
||||
/**
|
||||
* Handles the placement of a new edge expansion on the frame
|
||||
* (into the the list of edges already on the frame). Also handles
|
||||
* the logic of existing-symbol checking. If we are expanding into
|
||||
* the existing symbol, then this cursor will not modify the value
|
||||
* on the frame but will only yield true when it's expansion matches.
|
||||
* Return value of this function indicates those situations. If we
|
||||
* are not expanding into an existing symbol, but a new one, this
|
||||
* function will always append the new_edge to the ones on the
|
||||
* frame and return EdgePlacementResult::YIELD.
|
||||
*/
|
||||
EdgePlacementResult HandleEdgePlacement(
|
||||
const EdgeAccessor &new_edge, std::vector<TypedValue> &edges_on_frame) {
|
||||
if (self_.existing_edge_) {
|
||||
// check if the edge to add corresponds to the existing set's
|
||||
// corresponding element
|
||||
|
||||
if (edges_on_frame.size() < edges_.size())
|
||||
return EdgePlacementResult::MISMATCH;
|
||||
|
||||
EdgeAccessor &edge_on_frame =
|
||||
edges_on_frame[edges_.size() - 1].Value<EdgeAccessor>();
|
||||
if (edge_on_frame != new_edge) return EdgePlacementResult::MISMATCH;
|
||||
|
||||
// the new_edge matches the corresponding frame element
|
||||
// check if it's the last one (in which case we can yield)
|
||||
// or a subset (in which case we continue expansion but don't yield)
|
||||
if (edges_on_frame.size() == edges_.size())
|
||||
return EdgePlacementResult::YIELD;
|
||||
else
|
||||
return EdgePlacementResult::PREFIX;
|
||||
|
||||
} else {
|
||||
// we are placing an edge on the frame
|
||||
// it is possible that there already exists an edge on the frame for
|
||||
// this level. if so first remove it
|
||||
debug_assert(edges_.size() > 0, "Edges are empty");
|
||||
edges_on_frame.resize(std::min(edges_on_frame.size(), edges_.size() - 1));
|
||||
edges_on_frame.emplace_back(new_edge);
|
||||
return EdgePlacementResult::YIELD;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a single expansion for the current state of this
|
||||
* VariableExpansionCursor.
|
||||
*
|
||||
* @return True if the expansion was a success and this Cursor's
|
||||
* consumer can consume it. False if the expansion failed. In that
|
||||
* case no more expansions are available from the current input
|
||||
* vertex and another Pull from the input cursor should be performed.
|
||||
*/
|
||||
bool Expand(Frame &frame) {
|
||||
// some expansions might not be valid due to
|
||||
// edge uniqueness, existing_edge, existing_node criterions,
|
||||
// so expand in a loop until either the input vertex is
|
||||
// exhausted or a valid variable-length expansion is available
|
||||
while (true) {
|
||||
// pop from the stack while there is stuff to pop and the current
|
||||
// level is exhausted
|
||||
while (!edges_.empty() && edges_it_.back() == edges_.back().end()) {
|
||||
edges_.pop_back();
|
||||
edges_it_.pop_back();
|
||||
}
|
||||
|
||||
// check if we exhausted everything, if so return false
|
||||
if (edges_.empty()) return false;
|
||||
|
||||
// we use this a lot
|
||||
// TODO handle optional + existing edge
|
||||
std::vector<TypedValue> &edges_on_frame =
|
||||
frame[self_.edge_symbol_].Value<std::vector<TypedValue>>();
|
||||
|
||||
// it is possible that edges_on_frame does not contain as many
|
||||
// elements as edges_ due to edge-uniqueness (when a whole layer
|
||||
// gets exhausted but no edges are valid). for that reason only
|
||||
// pop from edges_on_frame if they contain enough elements
|
||||
if (!self_.existing_edge_)
|
||||
edges_on_frame.resize(std::min(edges_on_frame.size(), edges_.size()));
|
||||
|
||||
// if we are here, we have a valid stack,
|
||||
// get the edge, increase the relevant iterator
|
||||
std::pair<EdgeAccessor, EdgeAtom::Direction> current_edge =
|
||||
*edges_it_.back()++;
|
||||
|
||||
// check edge-uniqueness
|
||||
// only needs to be done if we're not expanding into existing edge
|
||||
// otherwise it gives false positives
|
||||
if (!self_.existing_edge_) {
|
||||
bool found_existing = std::any_of(
|
||||
edges_on_frame.begin(), edges_on_frame.end(),
|
||||
[¤t_edge](const TypedValue &edge) {
|
||||
return current_edge.first == edge.Value<EdgeAccessor>();
|
||||
});
|
||||
if (found_existing) continue;
|
||||
}
|
||||
|
||||
auto edge_placement_result =
|
||||
HandleEdgePlacement(current_edge.first, edges_on_frame);
|
||||
if (edge_placement_result == EdgePlacementResult::MISMATCH) continue;
|
||||
|
||||
VertexAccessor current_vertex =
|
||||
current_edge.second == EdgeAtom::Direction::IN
|
||||
? current_edge.first.from()
|
||||
: current_edge.first.to();
|
||||
|
||||
if (!self_.HandleExistingNode(current_vertex, frame)) continue;
|
||||
|
||||
// we are doing depth-first search, so place the current
|
||||
// edge's expansions onto the stack, if we should continue to expand
|
||||
if (!self_.upper_bound_ || self_.upper_bound_.value() > edges_.size()) {
|
||||
edges_.emplace_back(ExpandFromVertex(current_vertex, self_.direction_));
|
||||
edges_it_.emplace_back(edges_.back().begin());
|
||||
}
|
||||
|
||||
// we only yield true if we satisfy the lower bound, and frame
|
||||
// edge placement indicated we are good
|
||||
auto bound_ok = !self_.lower_bound_ ||
|
||||
edges_on_frame.size() >= self_.lower_bound_.value();
|
||||
if (bound_ok && edge_placement_result == EdgePlacementResult::YIELD)
|
||||
return true;
|
||||
else
|
||||
continue;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
std::unique_ptr<Cursor> ExpandVariable::MakeCursor(GraphDbAccessor &db) {
|
||||
return std::make_unique<ExpandVariableCursor>(*this, db);
|
||||
}
|
||||
|
||||
Filter::Filter(const std::shared_ptr<LogicalOperator> &input,
|
||||
@ -917,19 +1195,48 @@ ExpandUniquenessFilter<TAccessor>::ExpandUniquenessFilterCursor::
|
||||
GraphDbAccessor &db)
|
||||
: self_(self), input_cursor_(self.input_->MakeCursor(db)) {}
|
||||
|
||||
namespace {
|
||||
/**
|
||||
* Returns true if:
|
||||
* - a and b are vertex values and are the same
|
||||
* - a and b are either edge or edge-list values, and there
|
||||
* is at least one matching edge in the two values
|
||||
*/
|
||||
template <typename TAccessor>
|
||||
bool ContainsSame(const TypedValue &a, const TypedValue &b);
|
||||
|
||||
template <>
|
||||
bool ContainsSame<VertexAccessor>(const TypedValue &a, const TypedValue &b) {
|
||||
return a.Value<VertexAccessor>() == b.Value<VertexAccessor>();
|
||||
}
|
||||
|
||||
template <>
|
||||
bool ContainsSame<EdgeAccessor>(const TypedValue &a, const TypedValue &b) {
|
||||
auto compare_to_list = [](const TypedValue &list, const TypedValue &other) {
|
||||
for (const TypedValue &list_elem : list.Value<std::vector<TypedValue>>())
|
||||
if (ContainsSame<EdgeAccessor>(list_elem, other)) return true;
|
||||
return false;
|
||||
};
|
||||
|
||||
if (a.type() == TypedValue::Type::List) return compare_to_list(a, b);
|
||||
if (b.type() == TypedValue::Type::List) return compare_to_list(b, a);
|
||||
|
||||
return a.Value<EdgeAccessor>() == b.Value<EdgeAccessor>();
|
||||
}
|
||||
} // annonymous namespace
|
||||
|
||||
template <typename TAccessor>
|
||||
bool ExpandUniquenessFilter<TAccessor>::ExpandUniquenessFilterCursor::Pull(
|
||||
Frame &frame, const SymbolTable &symbol_table) {
|
||||
auto expansion_ok = [&]() {
|
||||
TypedValue &expand_value = frame[self_.expand_symbol_];
|
||||
TAccessor &expand_accessor = expand_value.Value<TAccessor>();
|
||||
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.
|
||||
TAccessor &previous_accessor = previous_value.Value<TAccessor>();
|
||||
if (expand_accessor == previous_accessor) return false;
|
||||
// TODO handle possible null due to optional match
|
||||
if (ContainsSame<TAccessor>(previous_value, expand_value)) return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
@ -2,10 +2,12 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <glog/logging.h>
|
||||
#include <algorithm>
|
||||
#include <experimental/optional>
|
||||
#include <memory>
|
||||
#include <tuple>
|
||||
#include <type_traits>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
@ -59,6 +61,7 @@ class ScanAllByLabel;
|
||||
class ScanAllByLabelPropertyRange;
|
||||
class ScanAllByLabelPropertyValue;
|
||||
class Expand;
|
||||
class ExpandVariable;
|
||||
class Filter;
|
||||
class Produce;
|
||||
class Delete;
|
||||
@ -83,9 +86,10 @@ class CreateIndex;
|
||||
|
||||
using LogicalOperatorCompositeVisitor = ::utils::CompositeVisitor<
|
||||
Once, CreateNode, CreateExpand, ScanAll, ScanAllByLabel,
|
||||
ScanAllByLabelPropertyRange, ScanAllByLabelPropertyValue, Expand, Filter,
|
||||
Produce, Delete, SetProperty, SetProperties, SetLabels, RemoveProperty,
|
||||
RemoveLabels, ExpandUniquenessFilter<VertexAccessor>,
|
||||
ScanAllByLabelPropertyRange, ScanAllByLabelPropertyValue, Expand,
|
||||
ExpandVariable, Filter, Produce, Delete, SetProperty, SetProperties,
|
||||
SetLabels, RemoveProperty, RemoveLabels,
|
||||
ExpandUniquenessFilter<VertexAccessor>,
|
||||
ExpandUniquenessFilter<EdgeAccessor>, Accumulate, AdvanceCommand, Aggregate,
|
||||
Skip, Limit, OrderBy, Merge, Optional, Unwind, Distinct>;
|
||||
|
||||
@ -431,6 +435,57 @@ class ScanAllByLabelPropertyValue : public ScanAll {
|
||||
Expression *expression_;
|
||||
};
|
||||
|
||||
/**
|
||||
* Common functionality and data members of single-edge
|
||||
* and variable-length expansion
|
||||
*/
|
||||
class ExpandCommon {
|
||||
protected:
|
||||
// types that we'll use for members in both subclasses
|
||||
using InEdgeT = decltype(std::declval<VertexAccessor>().in());
|
||||
using InEdgeIteratorT = decltype(std::declval<VertexAccessor>().in().begin());
|
||||
using OutEdgeT = decltype(std::declval<VertexAccessor>().out());
|
||||
using OutEdgeIteratorT =
|
||||
decltype(std::declval<VertexAccessor>().out().begin());
|
||||
|
||||
public:
|
||||
ExpandCommon(Symbol node_symbol, Symbol edge_symbol,
|
||||
EdgeAtom::Direction direction,
|
||||
const std::shared_ptr<LogicalOperator> &input,
|
||||
Symbol input_symbol, bool existing_node, bool existing_edge,
|
||||
GraphView graph_view = GraphView::AS_IS);
|
||||
|
||||
protected:
|
||||
// info on what's getting expanded
|
||||
const Symbol node_symbol_;
|
||||
const Symbol edge_symbol_;
|
||||
const EdgeAtom::Direction direction_;
|
||||
|
||||
// the input op and the symbol under which the op's result
|
||||
// can be found in the frame
|
||||
const std::shared_ptr<LogicalOperator> input_;
|
||||
const Symbol input_symbol_;
|
||||
|
||||
// if the given node and edge atom refer to symbols
|
||||
// (query identifiers) that have already been expanded
|
||||
// and should be just validated in the frame
|
||||
const bool existing_node_;
|
||||
const bool existing_edge_;
|
||||
|
||||
// from which state the input node should get expanded
|
||||
const GraphView graph_view_;
|
||||
|
||||
/**
|
||||
* For a newly expanded node handles existence checking and
|
||||
* frame placement.
|
||||
*
|
||||
* @return If or not the given new_node is a valid expansion. It is not
|
||||
* valid only when matching and existing node and new_node does not match
|
||||
* the old.
|
||||
*/
|
||||
bool HandleExistingNode(const VertexAccessor &new_node, Frame &frame) const;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Expansion operator. For a node existing in the frame it
|
||||
* expands one edge and one node and places them on the frame.
|
||||
@ -445,13 +500,7 @@ class ScanAllByLabelPropertyValue : public ScanAll {
|
||||
* only expansions that match defined equalities are succesfully
|
||||
* pulled.
|
||||
*/
|
||||
class Expand : public LogicalOperator {
|
||||
using InEdgeT = decltype(std::declval<VertexAccessor>().in());
|
||||
using InEdgeIteratorT = decltype(std::declval<VertexAccessor>().in().begin());
|
||||
using OutEdgeT = decltype(std::declval<VertexAccessor>().out());
|
||||
using OutEdgeIteratorT =
|
||||
decltype(std::declval<VertexAccessor>().out().begin());
|
||||
|
||||
class Expand : public LogicalOperator, ExpandCommon {
|
||||
public:
|
||||
/**
|
||||
* @brief Creates an expansion.
|
||||
@ -479,33 +528,11 @@ class Expand : public LogicalOperator {
|
||||
* in the Frame and should just be checked for equality.
|
||||
* @param existing_edge Same like `existing_node`, but for edges.
|
||||
*/
|
||||
Expand(Symbol node_symbol, Symbol edge_symbol, EdgeAtom::Direction direction,
|
||||
const std::shared_ptr<LogicalOperator> &input, Symbol input_symbol,
|
||||
bool existing_node, bool existing_edge,
|
||||
GraphView graph_view = GraphView::AS_IS);
|
||||
using ExpandCommon::ExpandCommon;
|
||||
|
||||
bool Accept(HierarchicalLogicalOperatorVisitor &visitor) override;
|
||||
std::unique_ptr<Cursor> MakeCursor(GraphDbAccessor &db) override;
|
||||
|
||||
private:
|
||||
// info on what's getting expanded
|
||||
const Symbol node_symbol_;
|
||||
const Symbol edge_symbol_;
|
||||
const EdgeAtom::Direction direction_;
|
||||
|
||||
// the input op and the symbol under which the op's result
|
||||
// can be found in the frame
|
||||
const std::shared_ptr<LogicalOperator> input_;
|
||||
const Symbol input_symbol_;
|
||||
|
||||
// if the given node and edge atom refer to symbols
|
||||
// (query identifiers) that have already been expanded
|
||||
// and should be just validated in the frame
|
||||
const bool existing_node_;
|
||||
const bool existing_edge_;
|
||||
|
||||
// from which state the input node should get expanded
|
||||
const GraphView graph_view_;
|
||||
|
||||
class ExpandCursor : public Cursor {
|
||||
public:
|
||||
ExpandCursor(const Expand &self, GraphDbAccessor &db);
|
||||
@ -515,6 +542,7 @@ class Expand : public LogicalOperator {
|
||||
private:
|
||||
const Expand &self_;
|
||||
const std::unique_ptr<Cursor> input_cursor_;
|
||||
GraphDbAccessor &db_;
|
||||
|
||||
// the iterable over edges and the current edge iterator are referenced via
|
||||
// unique pointers because they can not be initialized in the constructor of
|
||||
@ -526,17 +554,6 @@ class Expand : public LogicalOperator {
|
||||
|
||||
bool InitEdges(Frame &frame, const SymbolTable &symbol_table);
|
||||
|
||||
/**
|
||||
* For a newly expanded edge handles existence checking and frame insertion.
|
||||
*
|
||||
* @return If or not the given new_edge is a valid expansion. It is not
|
||||
* valid only when matching and existing edge and new_edge does not match
|
||||
* the
|
||||
* old.
|
||||
*/
|
||||
bool HandleExistingEdge(const EdgeAccessor &new_edge, Frame &frame,
|
||||
const SymbolTable &symbol_table);
|
||||
|
||||
/**
|
||||
* Expands a node for the given newly expanded edge.
|
||||
*
|
||||
@ -545,28 +562,102 @@ class Expand : public LogicalOperator {
|
||||
* new node does not qualify.
|
||||
*/
|
||||
bool PullNode(const EdgeAccessor &new_edge, EdgeAtom::Direction direction,
|
||||
Frame &frame, const SymbolTable &symbol_table);
|
||||
Frame &frame);
|
||||
|
||||
/**
|
||||
* For a newly expanded node handles existence checking and frame insertion.
|
||||
* For a newly expanded edge handles existence checking and
|
||||
* frame placement.
|
||||
*
|
||||
* @return If or not the given new_node is a valid expansion. It is not
|
||||
* valid only when matching and existing node and new_node does not match
|
||||
* the
|
||||
* old.
|
||||
* @return If or not the given new_edge is a valid expansion. It is not
|
||||
* valid only when matching and existing edge and new_edge does not match
|
||||
* the old.
|
||||
*/
|
||||
bool HandleExistingNode(const VertexAccessor new_node, Frame &frame,
|
||||
const SymbolTable &symbol_table);
|
||||
|
||||
GraphDbAccessor &db_;
|
||||
bool HandleExistingEdge(const EdgeAccessor &new_edge, Frame &frame) const;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Variable-length expansion operator. For a node existing in
|
||||
* the frame it expands a variable number of edges and places them
|
||||
* (in a list-type TypedValue), as well as the final destination node,
|
||||
* on the frame.
|
||||
*
|
||||
* This class does not handle node/edge filtering based on
|
||||
* properties, labels and edge types. However, it does handle
|
||||
* filtering on existing node / edge. Additionally it handles's
|
||||
* edge-uniquess (cyphermorphism) because it's not feasable to do
|
||||
* later.
|
||||
*
|
||||
* Filtering on existing means that for a pattern that references
|
||||
* an already declared node or edge (for example in
|
||||
* MATCH (a) MATCH (a)--(b)),
|
||||
* only expansions that match defined equalities are succesfully
|
||||
* pulled.
|
||||
*/
|
||||
class ExpandVariable : public LogicalOperator, ExpandCommon {
|
||||
// the ExpandVariableCursor is not declared in the header because
|
||||
// it's edges_ and edges_it_ are decltyped using a helper function
|
||||
// that should be inaccessible (private class function won't compile)
|
||||
friend class ExpandVariableCursor;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Creates a variable-length expansion.
|
||||
*
|
||||
* Expansion length bounds are both inclusive (as in Neo's Cypher
|
||||
* implementation). At least one of them has to be specified.
|
||||
*
|
||||
* Edge/Node existence is controlled via booleans. A true value
|
||||
* simply denotes that this expansion references an already
|
||||
* Pulled node/edge, and should only be checked for equalities
|
||||
* during expansion.
|
||||
*
|
||||
* Expansion can be done from old or new state of the vertex
|
||||
* the expansion originates from. This is controlled with a
|
||||
* constructor argument.
|
||||
*
|
||||
* @param node_symbol Symbol pointing to the node to be expanded. This is
|
||||
* where the new node will be stored.
|
||||
* @param edge_symbol Symbol for the edges to be expanded. This is where
|
||||
* a TypedValue containing a list of expanded edges will be stored.
|
||||
* @param direction EdgeAtom::Direction determining the direction of edge
|
||||
* expansion. The direction is relative to the starting vertex for each
|
||||
* expansion.
|
||||
* @param lower_bound An optional indicator of the minimum number of edges
|
||||
* that get expanded (inclusive).
|
||||
* @param lower_bound An optional indicator of the maximum number of edges
|
||||
* that get expanded (inclusive).
|
||||
* @param input Optional LogicalOperator that preceeds this one.
|
||||
* @param input_symbol Symbol that points to a VertexAccessor in the Frame
|
||||
* that expansion should emanate from.
|
||||
* @param existing_node If or not the node to be expanded is already present
|
||||
* in the Frame and should just be checked for equality.
|
||||
* @param existing_edge Same like `existing_node`, but for edges.
|
||||
*/
|
||||
// TODO change API to take expression instead of size_t
|
||||
ExpandVariable(Symbol node_symbol, Symbol edge_symbol,
|
||||
EdgeAtom::Direction direction,
|
||||
std::experimental::optional<size_t> lower_bound,
|
||||
std::experimental::optional<size_t> upper_bound,
|
||||
const std::shared_ptr<LogicalOperator> &input,
|
||||
Symbol input_symbol, bool existing_node, bool existing_edge,
|
||||
GraphView graph_view = GraphView::AS_IS);
|
||||
|
||||
bool Accept(HierarchicalLogicalOperatorVisitor &visitor) override;
|
||||
std::unique_ptr<Cursor> MakeCursor(GraphDbAccessor &db) override;
|
||||
|
||||
private:
|
||||
// lower and upper bounds of the variable length expansion are
|
||||
// optional, but at least one must be set
|
||||
const std::experimental::optional<size_t> lower_bound_;
|
||||
const std::experimental::optional<size_t> upper_bound_;
|
||||
};
|
||||
/**
|
||||
* @brief Filter whose Pull returns true only when the given expression
|
||||
* evaluates into true.
|
||||
*
|
||||
* The given expression is assumed to return either NULL (treated as false) or a
|
||||
* The given expression is assumed to return either NULL (treated as false) or
|
||||
* a
|
||||
* boolean value.
|
||||
*/
|
||||
class Filter : public LogicalOperator {
|
||||
@ -667,8 +758,8 @@ class Delete : public LogicalOperator {
|
||||
/**
|
||||
* @brief Logical Op for setting a single property on a single vertex or edge.
|
||||
*
|
||||
* The property value is an expression that must evaluate to some type that can
|
||||
* be stored (a TypedValue that can be converted to PropertyValue).
|
||||
* The property value is an expression that must evaluate to some type that
|
||||
* can be stored (a TypedValue that can be converted to PropertyValue).
|
||||
*/
|
||||
class SetProperty : public LogicalOperator {
|
||||
public:
|
||||
@ -699,7 +790,8 @@ class SetProperty : public LogicalOperator {
|
||||
* @brief Logical op for setting the whole properties set on a vertex or an
|
||||
* edge.
|
||||
*
|
||||
* The value being set is an expression that must evaluate to a vertex, edge or
|
||||
* The value being set is an expression that must evaluate to a vertex, edge
|
||||
* or
|
||||
* map (literal or parameter).
|
||||
*
|
||||
* Supports setting (replacing the whole properties set with another) and
|
||||
@ -710,8 +802,10 @@ class SetProperties : public LogicalOperator {
|
||||
/**
|
||||
* @brief Defines how setting the properties works.
|
||||
*
|
||||
* @c UPDATE means that the current property set is augmented with additional
|
||||
* ones (existing props of the same name are replaced), while @c REPLACE means
|
||||
* @c UPDATE means that the current property set is augmented with
|
||||
* additional
|
||||
* ones (existing props of the same name are replaced), while @c REPLACE
|
||||
* means
|
||||
* that the old props are discarded and replaced with new ones.
|
||||
*/
|
||||
enum class Op { UPDATE, REPLACE };
|
||||
@ -857,6 +951,8 @@ class RemoveLabels : public LogicalOperator {
|
||||
*
|
||||
* Works for both Edge and Vertex uniqueness checks
|
||||
* (provide the accessor type as a template argument).
|
||||
* Supports variable-length-edges (uniqueness comparisons
|
||||
* between edges and an edge lists).
|
||||
*/
|
||||
template <typename TAccessor>
|
||||
class ExpandUniquenessFilter : public LogicalOperator {
|
||||
|
@ -7,8 +7,8 @@
|
||||
|
||||
#include "database/graph_db.hpp"
|
||||
#include "storage/record_accessor.hpp"
|
||||
#include "storage/vertex.hpp"
|
||||
#include "storage/util.hpp"
|
||||
#include "storage/vertex.hpp"
|
||||
|
||||
#include "storage/edge_accessor.hpp"
|
||||
|
||||
@ -61,17 +61,21 @@ class VertexAccessor : public RecordAccessor<Vertex> {
|
||||
* Returns all the Labels of the Vertex.
|
||||
* @return
|
||||
*/
|
||||
const std::vector<GraphDbTypes::Label>& labels() const;
|
||||
const std::vector<GraphDbTypes::Label> &labels() const;
|
||||
|
||||
/**
|
||||
* Returns EdgeAccessors for all incoming edges.
|
||||
*/
|
||||
auto in() { return make_accessor_iterator<EdgeAccessor>(current().in_, db_accessor()); }
|
||||
auto in() const {
|
||||
return make_accessor_iterator<EdgeAccessor>(current().in_, db_accessor());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns EdgeAccessors for all outgoing edges.
|
||||
*/
|
||||
auto out() { return make_accessor_iterator<EdgeAccessor>(current().out_, db_accessor()); }
|
||||
auto out() const {
|
||||
return make_accessor_iterator<EdgeAccessor>(current().out_, db_accessor());
|
||||
}
|
||||
};
|
||||
|
||||
std::ostream &operator<<(std::ostream &, const VertexAccessor &);
|
||||
|
@ -3,10 +3,14 @@
|
||||
// Created by Florijan Stamenkovic on 14.03.17.
|
||||
//
|
||||
|
||||
#include <experimental/optional>
|
||||
#include <iterator>
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include "gmock/gmock.h"
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
@ -270,6 +274,268 @@ TEST(QueryPlan, Expand) {
|
||||
EXPECT_EQ(8, test_expand(EdgeAtom::Direction::BOTH, GraphView::OLD));
|
||||
}
|
||||
|
||||
/**
|
||||
* A fixture that sets a graph up and provides some functions.
|
||||
*
|
||||
* The graph is a double chain:
|
||||
* (v:0)-(v:1)-(v:2)
|
||||
* X X
|
||||
* (v:0)-(v:1)-(v:2)
|
||||
*
|
||||
* Each vertex is labeled (the labels are available as a
|
||||
* member in this class). Edges have properties set that
|
||||
* indicate origin and destination vertex for debugging.
|
||||
*/
|
||||
class QueryPlanExpandVariable : public testing::Test {
|
||||
protected:
|
||||
// type returned by the GetResults function, used
|
||||
// a lot below in test declaration
|
||||
using map_int = std::unordered_map<int, int>;
|
||||
|
||||
Dbms dbms;
|
||||
std::unique_ptr<GraphDbAccessor> dba = dbms.active();
|
||||
// labels for layers in the double chain
|
||||
std::vector<GraphDbTypes::Label> labels;
|
||||
|
||||
AstTreeStorage storage;
|
||||
SymbolTable symbol_table;
|
||||
|
||||
// using std::experimental::nullopt
|
||||
std::experimental::nullopt_t nullopt = std::experimental::nullopt;
|
||||
|
||||
void SetUp() {
|
||||
// create the graph
|
||||
int chain_length = 3;
|
||||
std::vector<VertexAccessor> layer;
|
||||
for (int from_layer_ind = -1; from_layer_ind < chain_length - 1;
|
||||
from_layer_ind++) {
|
||||
std::vector<VertexAccessor> new_layer{dba->insert_vertex(),
|
||||
dba->insert_vertex()};
|
||||
auto label = dba->label(std::to_string(from_layer_ind + 1));
|
||||
labels.push_back(label);
|
||||
for (size_t v_to_ind = 0; v_to_ind < new_layer.size(); v_to_ind++) {
|
||||
auto &v_to = new_layer[v_to_ind];
|
||||
v_to.add_label(label);
|
||||
for (size_t v_from_ind = 0; v_from_ind < layer.size(); v_from_ind++) {
|
||||
auto &v_from = layer[v_from_ind];
|
||||
auto edge =
|
||||
dba->insert_edge(v_from, v_to, dba->edge_type("edge_type"));
|
||||
edge.PropsSet(dba->property("p"),
|
||||
fmt::format("V{}{}->V{}{}", from_layer_ind, v_from_ind,
|
||||
from_layer_ind + 1, v_to_ind));
|
||||
}
|
||||
}
|
||||
layer = new_layer;
|
||||
}
|
||||
dba->advance_command();
|
||||
ASSERT_EQ(CountIterable(dba->vertices(false)), 2 * chain_length);
|
||||
ASSERT_EQ(CountIterable(dba->edges(false)), 4 * (chain_length - 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* Expands the given LogicalOperator input with a match
|
||||
* (ScanAll->Filter(label)->Expand). Can create both VariableExpand
|
||||
* ops and plain Expand (depending on template param).
|
||||
* When creating plain Expand the bound arguments (lower, upper) are ignored.
|
||||
*
|
||||
* @return the last created logical op.
|
||||
*/
|
||||
template <typename TExpansionOperator>
|
||||
std::shared_ptr<LogicalOperator> AddMatch(
|
||||
std::shared_ptr<LogicalOperator> input_op, const std::string &node_from,
|
||||
int layer, EdgeAtom::Direction direction,
|
||||
std::experimental::optional<size_t> lower,
|
||||
std::experimental::optional<size_t> upper, Symbol edge_sym,
|
||||
bool existing_edge, const std::string &node_to) {
|
||||
auto n_from = MakeScanAll(storage, symbol_table, node_from, input_op);
|
||||
auto filter_op = std::make_shared<Filter>(
|
||||
n_from.op_, storage.Create<query::LabelsTest>(
|
||||
n_from.node_->identifier_,
|
||||
std::vector<GraphDbTypes::Label>{labels[layer]}));
|
||||
|
||||
auto n_to = NODE(node_to);
|
||||
auto n_to_sym = symbol_table.CreateSymbol(node_to, true);
|
||||
symbol_table[*n_to->identifier_] = n_to_sym;
|
||||
|
||||
if (std::is_same<TExpansionOperator, ExpandVariable>::value)
|
||||
return std::make_shared<ExpandVariable>(
|
||||
n_to_sym, edge_sym, direction, lower, upper, filter_op, n_from.sym_,
|
||||
false, existing_edge, GraphView::OLD);
|
||||
else
|
||||
return std::make_shared<Expand>(n_to_sym, edge_sym, direction, filter_op,
|
||||
n_from.sym_, false, existing_edge,
|
||||
GraphView::OLD);
|
||||
}
|
||||
|
||||
/* Creates an edge (in the frame and symbol table). Returns the symbol. */
|
||||
auto Edge(const std::string &identifier, EdgeAtom::Direction direction) {
|
||||
auto edge = EDGE(identifier, direction);
|
||||
auto edge_sym = symbol_table.CreateSymbol(identifier, true);
|
||||
symbol_table[*edge->identifier_] = edge_sym;
|
||||
return edge_sym;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pulls from the given input and analyses the edge-list (result of variable
|
||||
* length expansion) found in the results under the given symbol.
|
||||
*
|
||||
* @return a map {path_lenth -> number_of_results}
|
||||
*/
|
||||
auto GetResults(std::shared_ptr<LogicalOperator> input_op, Symbol symbol) {
|
||||
map_int count_per_length;
|
||||
Frame frame(symbol_table.max_position());
|
||||
auto cursor = input_op->MakeCursor(*dba);
|
||||
while (cursor->Pull(frame, symbol_table)) {
|
||||
auto length = frame[symbol].Value<std::vector<TypedValue>>().size();
|
||||
auto found = count_per_length.find(length);
|
||||
if (found == count_per_length.end())
|
||||
count_per_length[length] = 1;
|
||||
else
|
||||
found->second++;
|
||||
}
|
||||
return count_per_length;
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(QueryPlanExpandVariable, OneVariableExpansion) {
|
||||
auto test_expand = [&](int layer, EdgeAtom::Direction direction,
|
||||
std::experimental::optional<size_t> lower,
|
||||
std::experimental::optional<size_t> upper) {
|
||||
auto e = Edge("r", direction);
|
||||
return GetResults(AddMatch<ExpandVariable>(nullptr, "n", layer, direction,
|
||||
lower, upper, e, false, "m"),
|
||||
e);
|
||||
};
|
||||
|
||||
EXPECT_EQ(test_expand(0, EdgeAtom::Direction::IN, 0, 0), (map_int{{0, 2}}));
|
||||
EXPECT_EQ(test_expand(0, EdgeAtom::Direction::OUT, 0, 0), (map_int{{0, 2}}));
|
||||
EXPECT_EQ(test_expand(0, EdgeAtom::Direction::BOTH, 0, 0), (map_int{{0, 2}}));
|
||||
EXPECT_EQ(test_expand(0, EdgeAtom::Direction::IN, 1, 1), (map_int{}));
|
||||
EXPECT_EQ(test_expand(0, EdgeAtom::Direction::OUT, 1, 1), (map_int{{1, 4}}));
|
||||
EXPECT_EQ(test_expand(1, EdgeAtom::Direction::IN, 1, 1), (map_int{{1, 4}}));
|
||||
EXPECT_EQ(test_expand(1, EdgeAtom::Direction::OUT, 1, 1), (map_int{{1, 4}}));
|
||||
EXPECT_EQ(test_expand(1, EdgeAtom::Direction::BOTH, 1, 1), (map_int{{1, 8}}));
|
||||
EXPECT_EQ(test_expand(0, EdgeAtom::Direction::OUT, 2, 2), (map_int{{2, 8}}));
|
||||
EXPECT_EQ(test_expand(0, EdgeAtom::Direction::OUT, 2, 3), (map_int{{2, 8}}));
|
||||
EXPECT_EQ(test_expand(0, EdgeAtom::Direction::OUT, 1, 2),
|
||||
(map_int{{1, 4}, {2, 8}}));
|
||||
|
||||
// the following tests also check edge-uniqueness (cyphermorphisim)
|
||||
EXPECT_EQ(test_expand(0, EdgeAtom::Direction::BOTH, 1, 2),
|
||||
(map_int{{1, 4}, {2, 12}}));
|
||||
EXPECT_EQ(test_expand(1, EdgeAtom::Direction::BOTH, 4, 4),
|
||||
(map_int{{4, 24}}));
|
||||
}
|
||||
|
||||
TEST_F(QueryPlanExpandVariable, EdgeUniquenessSingleAndVariableExpansion) {
|
||||
auto test_expand = [&](int layer, EdgeAtom::Direction direction,
|
||||
std::experimental::optional<size_t> lower,
|
||||
std::experimental::optional<size_t> upper,
|
||||
bool single_expansion_before,
|
||||
bool add_uniqueness_check) {
|
||||
|
||||
std::shared_ptr<LogicalOperator> last_op{nullptr};
|
||||
std::vector<Symbol> symbols;
|
||||
|
||||
if (single_expansion_before) {
|
||||
symbols.push_back(Edge("r0", direction));
|
||||
last_op = AddMatch<Expand>(last_op, "n0", layer, direction, lower, upper,
|
||||
symbols.back(), false, "m0");
|
||||
}
|
||||
|
||||
auto var_length_sym = Edge("r1", direction);
|
||||
symbols.push_back(var_length_sym);
|
||||
last_op = AddMatch<ExpandVariable>(last_op, "n1", layer, direction, lower,
|
||||
upper, var_length_sym, false, "m1");
|
||||
|
||||
if (!single_expansion_before) {
|
||||
symbols.push_back(Edge("r2", direction));
|
||||
last_op = AddMatch<Expand>(last_op, "n2", layer, direction, lower, upper,
|
||||
symbols.back(), false, "m2");
|
||||
}
|
||||
|
||||
if (add_uniqueness_check) {
|
||||
auto last_symbol = symbols.back();
|
||||
symbols.pop_back();
|
||||
last_op = std::make_shared<ExpandUniquenessFilter<EdgeAccessor>>(
|
||||
last_op, last_symbol, symbols);
|
||||
}
|
||||
|
||||
return GetResults(last_op, var_length_sym);
|
||||
};
|
||||
|
||||
// no uniqueness between variable and single expansion
|
||||
EXPECT_EQ(test_expand(0, EdgeAtom::Direction::OUT, 2, 3, true, false),
|
||||
(map_int{{2, 4 * 8}}));
|
||||
// with uniqueness test, different ordering of (variable, single) expansion
|
||||
EXPECT_EQ(test_expand(0, EdgeAtom::Direction::OUT, 2, 3, true, true),
|
||||
(map_int{{2, 3 * 8}}));
|
||||
EXPECT_EQ(test_expand(0, EdgeAtom::Direction::OUT, 2, 3, false, true),
|
||||
(map_int{{2, 3 * 8}}));
|
||||
}
|
||||
|
||||
TEST_F(QueryPlanExpandVariable, EdgeUniquenessTwoVariableExpansions) {
|
||||
auto test_expand = [&](int layer, EdgeAtom::Direction direction,
|
||||
std::experimental::optional<size_t> lower,
|
||||
std::experimental::optional<size_t> upper,
|
||||
bool add_uniqueness_check) {
|
||||
auto e1 = Edge("r1", direction);
|
||||
auto first = AddMatch<ExpandVariable>(nullptr, "n1", layer, direction,
|
||||
lower, upper, e1, false, "m1");
|
||||
auto e2 = Edge("r2", direction);
|
||||
auto last_op = AddMatch<ExpandVariable>(first, "n2", layer, direction,
|
||||
lower, upper, e2, false, "m2");
|
||||
if (add_uniqueness_check) {
|
||||
last_op = std::make_shared<ExpandUniquenessFilter<EdgeAccessor>>(
|
||||
last_op, e2, std::vector<Symbol>{e1});
|
||||
}
|
||||
|
||||
return GetResults(last_op, e2);
|
||||
};
|
||||
|
||||
EXPECT_EQ(test_expand(0, EdgeAtom::Direction::OUT, 2, 2, false),
|
||||
(map_int{{2, 8 * 8}}));
|
||||
EXPECT_EQ(test_expand(0, EdgeAtom::Direction::OUT, 2, 2, true),
|
||||
(map_int{{2, 5 * 8}}));
|
||||
}
|
||||
|
||||
TEST_F(QueryPlanExpandVariable, ExistingEdges) {
|
||||
auto test_expand = [&](int layer, EdgeAtom::Direction direction,
|
||||
std::experimental::optional<size_t> lower,
|
||||
std::experimental::optional<size_t> upper,
|
||||
bool same_edge_symbol) {
|
||||
auto e1 = Edge("r1", direction);
|
||||
auto first = AddMatch<ExpandVariable>(nullptr, "n1", layer, direction,
|
||||
lower, upper, e1, false, "m1");
|
||||
auto e2 = same_edge_symbol ? e1 : Edge("r2", direction);
|
||||
auto second = AddMatch<ExpandVariable>(first, "n2", layer, direction, lower,
|
||||
upper, e2, same_edge_symbol, "m2");
|
||||
return GetResults(second, e2);
|
||||
};
|
||||
|
||||
EXPECT_EQ(test_expand(0, EdgeAtom::Direction::OUT, 1, 1, false),
|
||||
(map_int{{1, 4 * 4}}));
|
||||
EXPECT_EQ(test_expand(0, EdgeAtom::Direction::OUT, 1, 1, true),
|
||||
(map_int{{1, 4}}));
|
||||
|
||||
EXPECT_EQ(test_expand(0, EdgeAtom::Direction::OUT, 0, 1, false),
|
||||
(map_int{{0, 2 * 6}, {1, 4 * 6}}));
|
||||
EXPECT_EQ(test_expand(0, EdgeAtom::Direction::OUT, 0, 1, true),
|
||||
(map_int{{0, 4}, {1, 4}}));
|
||||
|
||||
EXPECT_EQ(test_expand(0, EdgeAtom::Direction::OUT, 2, 3, false),
|
||||
(map_int{{2, 8 * 8}}));
|
||||
EXPECT_EQ(test_expand(0, EdgeAtom::Direction::OUT, 2, 3, true),
|
||||
(map_int{{2, 8}}));
|
||||
|
||||
EXPECT_EQ(test_expand(0, EdgeAtom::Direction::BOTH, 1, 2, false),
|
||||
(map_int{{1, 4 * 16}, {2, 12 * 16}}));
|
||||
EXPECT_EQ(test_expand(0, EdgeAtom::Direction::BOTH, 1, 2, true),
|
||||
(map_int{{1, 4}, {2, 12}}));
|
||||
}
|
||||
|
||||
// TODO test optional + variable length
|
||||
|
||||
TEST(QueryPlan, ExpandOptional) {
|
||||
Dbms dbms;
|
||||
auto dba = dbms.active();
|
||||
@ -395,6 +661,7 @@ TEST(QueryPlan, OptionalMatchThenExpandToMissingNode) {
|
||||
auto n = MakeScanAll(storage, symbol_table, "n");
|
||||
auto label_missing = dba->label("missing");
|
||||
n.node_->labels_.emplace_back(label_missing);
|
||||
|
||||
auto *filter_expr =
|
||||
storage.Create<LabelsTest>(n.node_->identifier_, n.node_->labels_);
|
||||
auto node_filter = std::make_shared<Filter>(n.op_, filter_expr);
|
||||
@ -876,7 +1143,7 @@ TEST(QueryPlan, ScanAllByLabelProperty) {
|
||||
symbol_table[*output] = symbol_table.CreateSymbol("n", true);
|
||||
auto results = CollectProduce(produce.get(), symbol_table, *dba);
|
||||
ASSERT_EQ(results.size(), expected.size());
|
||||
for (int i = 0; i < expected.size(); i++) {
|
||||
for (size_t i = 0; i < expected.size(); i++) {
|
||||
TypedValue equal =
|
||||
results[i][0].Value<VertexAccessor>().PropsAt(prop) == expected[i];
|
||||
ASSERT_EQ(equal.type(), TypedValue::Type::Bool);
|
||||
@ -956,7 +1223,8 @@ TEST(QueryPlan, ScanAllByLabelPropertyEqualityNoError) {
|
||||
TEST(QueryPlan, ScanAllByLabelPropertyEqualNull) {
|
||||
Dbms dbms;
|
||||
auto dba = dbms.active();
|
||||
// Add 2 vertices with the same label, but one has a property value while the
|
||||
// Add 2 vertices with the same label, but one has a property value while
|
||||
// the
|
||||
// other does not. Checking if the value is equal to null, should yield no
|
||||
// results.
|
||||
auto label = dba->label("label");
|
||||
@ -984,3 +1252,10 @@ TEST(QueryPlan, ScanAllByLabelPropertyEqualNull) {
|
||||
auto results = CollectProduce(produce.get(), symbol_table, *dba);
|
||||
EXPECT_EQ(results.size(), 0);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
google::InitGoogleLogging(argv[0]);
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
google::ParseCommandLineFlags(&argc, &argv, true);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user