Named path support

Reviewers: buda, teon.banek, mislav.bradac

Reviewed By: teon.banek

Subscribers: pullbot

Differential Revision: https://phabricator.memgraph.io/D794
This commit is contained in:
florijan 2017-09-18 14:40:36 +02:00
parent a2c56dd83e
commit 347611edfd
26 changed files with 641 additions and 148 deletions

View File

@ -5,17 +5,18 @@
### Major Features and Improvements
* CASE construct (without aggregations).
* `rand` function added.
* Named path support added.
* Maps can now be stored as vertex/edge properties.
* `collect` aggregation now supports Map collection.
* Map indexing supported.
* `rand` function added.
* `assert` function added.
* Use \u to specify 4 digit codepoint and \U for 8 digit
* `counter` and `counterSet` functions added.
* `indexInfo` function added.
* `collect` aggregation now supports Map collection.
### Bug Fixes and Other Changes
* Use \u to specify 4 digit codepoint and \U for 8 digit
* Keywords appearing in header (named expressions) keep original case.
* Our Bolt protocol implementation is now completely compatible with the protocol version 1 specification. (https://boltprotocol.org/v1/)

View File

@ -51,17 +51,22 @@ There are cases when a user needs to find data which is connected by
traversing a path of connections, but the user doesn't know how many
connections need to be traversed. openCypher allows for designating patterns
with *variable path lengths*. Matching such a path is achieved by using the
`*` (*asterisk*) symbol inside the pattern for a connection. For example,
`*` (*asterisk*) symbol inside the edge element of a pattern. For example,
traversing from `node1` to `node2` by following any number of connections in a
single direction can be achieved with:
MATCH (node1) -[*]-> (node2)
MATCH (node1) -[r*]-> (node2) RETURN node1, r, node2
If paths are very long, finding them could take a long time. To prevent that,
a user can provide the minimum and maximum length of the path. For example,
paths of length between 2 and 4 can be obtained with a query like:
MATCH (node1) -[*2..4]-> (node2)
MATCH (node1) -[r*2..4]-> (node2) RETURN node1, r, node2
It is possible to name patterns in the query and return the resulting paths.
This is especially useful when matching variable length paths:
MATCH path = () -[r*2..4]-> () RETURN path
More details on how `MATCH` works can be found
[here](https://neo4j.com/docs/developer-manual/current/cypher/clauses/match/).
@ -467,7 +472,7 @@ functions.
`head` | Returns the first element of a list.
`last` | Returns the last element of a list.
`properties` | Returns the properties of a node or an edge.
`size` | Returns the number of elements in a list.
`size` | Returns the number of elements in a list or a map. When given a string it returns the number of characters. When given a path it returns the number of expansions (edges) in that path.
`toBoolean` | Converts the argument to a boolean.
`toFloat` | Converts the argument to a floating point number.
`toInteger` | Converts the argument to an integer.

View File

@ -26,19 +26,6 @@ allow deletion of created indices.
Although we have implemented the most common features of the openCypher query
language, there are other useful features we are still working on.
#### Named Paths
It would be useful to store paths that match a pattern into a variable. This
enables the user to display the matched patterns or do some other operations
on the path, like calculating the length of the path.
The feature would be used by simply assigning the variable to a pattern. For
example:
MATCH path = (node1) -[connection]-> (node2)
Path naming is especially useful with the *variable length paths* feature.
#### Functions
Memgraph's openCypher implementation supports the most useful functions, but

View File

@ -1125,6 +1125,7 @@ class Pattern : public Tree {
DEFVISITABLE(TreeVisitor<TypedValue>);
bool Accept(HierarchicalTreeVisitor &visitor) override {
if (visitor.PreVisit(*this)) {
identifier_->Accept(visitor);
for (auto &part : atoms_) {
if (!part->Accept(visitor)) break;
}

View File

@ -190,7 +190,12 @@ SymbolGenerator::ReturnType SymbolGenerator::Visit(Identifier &ident) {
scope_.in_skip ? "SKIP" : "LIMIT");
}
Symbol symbol;
if (scope_.in_pattern && scope_.in_pattern_identifier) {
if (scope_.in_pattern && !(scope_.in_node_atom || scope_.visiting_edge)) {
// If we are in the pattern, and outside of a node or an edge, the
// identifier is the pattern name.
symbol = GetOrCreateSymbol(ident.name_, ident.user_declared_,
Symbol::Type::Path);
} else if (scope_.in_pattern && scope_.in_pattern_atom_identifier) {
// Patterns can bind new symbols or reference already bound. But there
// are the following special cases:
// 1) Patterns used to create nodes and edges cannot redeclare already
@ -221,7 +226,7 @@ SymbolGenerator::ReturnType SymbolGenerator::Visit(Identifier &ident) {
}
}
symbol = GetOrCreateSymbol(ident.name_, ident.user_declared_, type);
} else if (scope_.in_pattern && !scope_.in_pattern_identifier &&
} else if (scope_.in_pattern && !scope_.in_pattern_atom_identifier &&
scope_.in_match) {
if (scope_.in_edge_range &&
scope_.visiting_edge->identifier_->name_ == ident.name_) {
@ -329,9 +334,9 @@ bool SymbolGenerator::PreVisit(NodeAtom &node_atom) {
for (auto kv : node_atom.properties_) {
kv.second->Accept(*this);
}
scope_.in_pattern_identifier = true;
scope_.in_pattern_atom_identifier = true;
node_atom.identifier_->Accept(*this);
scope_.in_pattern_identifier = false;
scope_.in_pattern_atom_identifier = false;
return false;
}
@ -374,9 +379,9 @@ bool SymbolGenerator::PreVisit(EdgeAtom &edge_atom) {
}
scope_.in_edge_range = false;
}
scope_.in_pattern_identifier = true;
scope_.in_pattern_atom_identifier = true;
edge_atom.identifier_->Accept(*this);
scope_.in_pattern_identifier = false;
scope_.in_pattern_atom_identifier = false;
return false;
}
@ -428,9 +433,9 @@ bool SymbolGenerator::PreVisit(BreadthFirstAtom &bf_atom) {
scope_.in_pattern = true;
// XXX: Make BFS symbol be EdgeList.
bf_atom.has_range_ = true;
scope_.in_pattern_identifier = true;
scope_.in_pattern_atom_identifier = true;
bf_atom.identifier_->Accept(*this);
scope_.in_pattern_identifier = false;
scope_.in_pattern_atom_identifier = false;
bf_atom.has_range_ = false;
return false;
}

View File

@ -83,9 +83,9 @@ class SymbolGenerator : public HierarchicalTreeVisitor {
bool in_order_by{false};
bool in_where{false};
bool in_match{false};
// True when visiting a pattern identifier, which can be reused or created
// in the pattern itself.
bool in_pattern_identifier{false};
// True when visiting a pattern atom (node or edge) identifier, which can be
// reused or created in the pattern itself.
bool in_pattern_atom_identifier{false};
// True when visiting range bounds of a variable path.
bool in_edge_range{false};
// True if the return/with contains an aggregation in any named expression.

View File

@ -64,6 +64,8 @@ class SymbolTable {
int max_position() const { return position_; }
const auto &table() const { return table_; }
private:
int position_{0};
std::map<int, Symbol> table_;

View File

@ -135,6 +135,8 @@ TypedValue Size(const std::vector<TypedValue> &args, GraphDbAccessor &) {
// to do it.
return static_cast<int64_t>(
args[0].Value<std::map<std::string, TypedValue>>().size());
case TypedValue::Type::Path:
return static_cast<int64_t>(args[0].ValuePath().edges().size());
default:
throw QueryRuntimeException("size called with incompatible type");
}

81
src/query/path.hpp Normal file
View File

@ -0,0 +1,81 @@
#pragma once
#include <functional>
#include <utility>
#include "storage/edge_accessor.hpp"
#include "storage/vertex_accessor.hpp"
#include "utils/assert.hpp"
namespace query {
/**
* A data structure that holds a graph path. A path consists of at least one
* vertex, followed by zero or more edge + vertex extensions (thus having one
* vertex more then edges).
*/
class Path {
public:
/** Creates the path starting with the given vertex. */
Path(const VertexAccessor &vertex) { Expand(vertex); }
/** Creates the path starting with the given vertex and containing all other
* elements. */
template <typename... TOthers>
Path(const VertexAccessor &vertex, const TOthers &... others) {
Expand(vertex);
Expand(others...);
}
/** Expands the path with the given vertex. */
void Expand(const VertexAccessor &vertex) {
debug_assert(vertices_.size() == edges_.size(),
"Illegal path construction order");
vertices_.emplace_back(vertex);
}
/** Expands the path with the given edge. */
void Expand(const EdgeAccessor &edge) {
debug_assert(vertices_.size() - 1 == edges_.size(),
"Illegal path construction order");
edges_.emplace_back(edge);
}
/** Expands the path with the given elements. */
template <typename TFirst, typename... TOthers>
void Expand(const TFirst &first, const TOthers &... others) {
Expand(first);
Expand(others...);
}
auto &vertices() { return vertices_; }
auto &edges() { return edges_; }
const auto &vertices() const { return vertices_; }
const auto &edges() const { return edges_; }
bool operator==(const Path &other) const {
return vertices_ == other.vertices_ && edges_ == other.edges_;
}
friend std::ostream &operator<<(std::ostream &os, const Path &path) {
debug_assert(path.vertices_.size() > 0U,
"Attempting to stream out an invalid path");
os << path.vertices_[0];
for (int i = 0; i < static_cast<int>(path.edges_.size()); i++) {
bool arrow_to_left = path.vertices_[i] == path.edges_[i].to();
if (arrow_to_left) os << "<";
os << "-" << path.edges_[i] << "-";
if (!arrow_to_left) os << ">";
os << path.vertices_[i + 1];
}
return os;
}
private:
// Contains all the vertices in the path.
std::vector<VertexAccessor> vertices_;
// Contains all the edges in the path (one less then there are vertices).
std::vector<EdgeAccessor> edges_;
};
} // namespace query

View File

@ -223,11 +223,12 @@ class CostEstimator : public HierarchicalLogicalOperatorVisitor {
std::experimental::optional<PropertyValue> ConstPropertyValue(
const Expression *expression) {
if (auto *literal = dynamic_cast<const PrimitiveLiteral *>(expression)) {
if (literal->value_.IsPropertyValue()) return literal->value_;
if (literal->value_.IsPropertyValue())
return static_cast<PropertyValue>(literal->value_);
} else if (auto *param_lookup =
dynamic_cast<const ParameterLookup *>(expression)) {
auto value = parameters.AtTokenPosition(param_lookup->token_position_);
if (value.IsPropertyValue()) return value;
if (value.IsPropertyValue()) return static_cast<PropertyValue>(value);
}
return std::experimental::nullopt;
}

View File

@ -313,10 +313,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);
};
@ -577,11 +577,6 @@ bool Expand::ExpandCursor::InitEdges(Frame &frame, Context &context) {
out_edges_it_.emplace(out_edges_->begin());
}
// TODO add support for Front and Back expansion (when QueryPlanner
// will need it). For now only Back expansion (left to right) is
// supported
// TODO add support for named paths
return true;
}
}
@ -1132,6 +1127,88 @@ void ExpandBreadthFirst::Cursor::Reset() {
to_visit_current_.clear();
}
class ConstructNamedPathCursor : public Cursor {
public:
ConstructNamedPathCursor(const ConstructNamedPath &self, GraphDbAccessor &db)
: self_(self), input_cursor_(self_.input()->MakeCursor(db)) {}
bool Pull(Frame &frame, Context &context) override {
if (!input_cursor_->Pull(frame, context)) return false;
auto symbol_it = self_.path_elements().begin();
debug_assert(symbol_it != self_.path_elements().end(),
"Named path must contain at least one node");
TypedValue start_vertex = frame[*symbol_it++];
// In an OPTIONAL MATCH everything could be Null.
if (start_vertex.IsNull()) {
frame[self_.path_symbol()] = TypedValue::Null;
return true;
}
debug_assert(start_vertex.IsVertex(),
"First named path element must be a vertex");
query::Path path(start_vertex.ValueVertex());
// If the last path element symbol was for an edge list, then
// the next symbol is a vertex and it should not append to the path because
// expansion already did it.
bool last_was_edge_list = false;
for (; symbol_it != self_.path_elements().end(); symbol_it++) {
TypedValue expansion = frame[*symbol_it];
// We can have Null (OPTIONAL MATCH), a vertex, an edge, or an edge
// list (variable expand or BFS).
switch (expansion.type()) {
case TypedValue::Type::Null:
frame[self_.path_symbol()] = TypedValue::Null;
return true;
case TypedValue::Type::Vertex:
if (!last_was_edge_list) path.Expand(expansion.ValueVertex());
last_was_edge_list = false;
break;
case TypedValue::Type::Edge:
path.Expand(expansion.ValueEdge());
break;
case TypedValue::Type::List: {
last_was_edge_list = true;
// We need to expand all edges in the list and intermediary vertices.
const std::vector<TypedValue> &edges = expansion.ValueList();
for (const auto &edge_value : edges) {
const EdgeAccessor &edge = edge_value.ValueEdge();
const VertexAccessor from = edge.from();
if (path.vertices().back() == from)
path.Expand(edge, edge.to());
else
path.Expand(edge, from);
}
break;
}
default:
permanent_fail("Unsupported type in named path construction");
break;
}
}
frame[self_.path_symbol()] = path;
return true;
}
void Reset() override { input_cursor_->Reset(); }
private:
const ConstructNamedPath self_;
const std::unique_ptr<Cursor> input_cursor_;
};
ACCEPT_WITH_INPUT(ConstructNamedPath)
std::unique_ptr<Cursor> ConstructNamedPath::MakeCursor(GraphDbAccessor &db) {
return std::make_unique<ConstructNamedPathCursor>(*this, db);
}
Filter::Filter(const std::shared_ptr<LogicalOperator> &input,
Expression *expression)
: input_(input ? input : std::make_shared<Once>()),
@ -1147,8 +1224,8 @@ Filter::FilterCursor::FilterCursor(const Filter &self, GraphDbAccessor &db)
: self_(self), db_(db), input_cursor_(self_.input_->MakeCursor(db)) {}
bool Filter::FilterCursor::Pull(Frame &frame, Context &context) {
// Like all filters, newly set values should not affect filtering of old nodes
// and edges.
// Like all filters, newly set values should not affect filtering of old
// nodes and edges.
ExpressionEvaluator evaluator(frame, context.parameters_,
context.symbol_table_, db_, GraphView::OLD);
while (input_cursor_->Pull(frame, context)) {
@ -1553,8 +1630,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.
// sure these are all of the expected type. In case they are not, an
// error should be raised long before this code is executed.
// TODO handle possible null due to optional match
if (ContainsSame<TAccessor>(previous_value, expand_value)) return false;
}
@ -1583,18 +1660,20 @@ namespace {
* given TypedValue.
*/
void ReconstructTypedValue(TypedValue &value) {
const static std::string vertex_error_msg =
"Vertex invalid after WITH clause, (most likely deleted by a "
"preceeding DELETE clause)";
const static std::string edge_error_msg =
"Edge invalid after WITH clause, (most likely deleted by a "
"preceeding DELETE clause)";
switch (value.type()) {
case TypedValue::Type::Vertex:
if (!value.Value<VertexAccessor>().Reconstruct())
throw QueryRuntimeException(
"Vertex invalid after WITH clause, (most likely deleted by a "
"preceeding DELETE clause)");
throw QueryRuntimeException(vertex_error_msg);
break;
case TypedValue::Type::Edge:
if (!value.Value<VertexAccessor>().Reconstruct())
throw QueryRuntimeException(
"Edge invalid after WITH clause, (most likely deleted by a "
"preceeding DELETE clause)");
throw QueryRuntimeException(edge_error_msg);
break;
case TypedValue::Type::List:
for (TypedValue &inner_value : value.Value<std::vector<TypedValue>>())
@ -1605,8 +1684,10 @@ void ReconstructTypedValue(TypedValue &value) {
ReconstructTypedValue(kv.second);
break;
case TypedValue::Type::Path:
// TODO implement path reconstruct?
throw utils::NotYetImplemented("path reconstruction");
for (auto &vertex : value.ValuePath().vertices())
if (vertex.Reconstruct()) throw QueryRuntimeException(vertex_error_msg);
for (auto &edge : value.ValuePath().edges())
if (edge.Reconstruct()) throw QueryRuntimeException(edge_error_msg);
default:
break;
}
@ -2413,8 +2494,8 @@ class CreateIndexCursor : public Cursor {
// Report to the end user.
did_create_ = false;
throw QueryRuntimeException(
"Index building already in progress on this database. Memgraph does "
"not support concurrent index building.");
"Index building already in progress on this database. Memgraph "
"does not support concurrent index building.");
}
did_create_ = true;
return true;

View File

@ -18,6 +18,8 @@
#include "query/common.hpp"
#include "query/exceptions.hpp"
#include "query/frontend/semantic/symbol_table.hpp"
#include "query/path.hpp"
#include "query/typed_value.hpp"
#include "utils/bound.hpp"
#include "utils/hashing/fnv.hpp"
#include "utils/visitor.hpp"
@ -67,6 +69,7 @@ class ScanAllByLabelPropertyValue;
class Expand;
class ExpandVariable;
class ExpandBreadthFirst;
class ConstructNamedPath;
class Filter;
class Produce;
class Delete;
@ -92,8 +95,8 @@ class CreateIndex;
using LogicalOperatorCompositeVisitor = ::utils::CompositeVisitor<
Once, CreateNode, CreateExpand, ScanAll, ScanAllByLabel,
ScanAllByLabelPropertyRange, ScanAllByLabelPropertyValue, Expand,
ExpandVariable, ExpandBreadthFirst, Filter, Produce, Delete, SetProperty,
SetProperties, SetLabels, RemoveProperty, RemoveLabels,
ExpandVariable, ExpandBreadthFirst, ConstructNamedPath, Filter, Produce,
Delete, SetProperty, SetProperties, SetLabels, RemoveProperty, RemoveLabels,
ExpandUniquenessFilter<VertexAccessor>,
ExpandUniquenessFilter<EdgeAccessor>, Accumulate, AdvanceCommand, Aggregate,
Skip, Limit, OrderBy, Merge, Optional, Unwind, Distinct>;
@ -731,6 +734,30 @@ class ExpandBreadthFirst : public LogicalOperator {
const GraphView graph_view_;
};
/**
* Constructs a named path from it's elements and places it on the frame.
*/
class ConstructNamedPath : public LogicalOperator {
public:
ConstructNamedPath(const std::shared_ptr<LogicalOperator> &input,
Symbol path_symbol,
const std::vector<Symbol> &path_elements)
: input_(input),
path_symbol_(path_symbol),
path_elements_(path_elements) {}
bool Accept(HierarchicalLogicalOperatorVisitor &visitor) override;
std::unique_ptr<Cursor> MakeCursor(GraphDbAccessor &db) override;
const auto &input() const { return input_; }
const auto &path_symbol() const { return path_symbol_; }
const auto &path_elements() const { return path_elements_; }
private:
const std::shared_ptr<LogicalOperator> input_;
const Symbol path_symbol_;
const std::vector<Symbol> path_elements_;
};
/**
* @brief Filter whose Pull returns true only when the given expression
* evaluates into true.

View File

@ -589,6 +589,13 @@ void AddMatching(const std::vector<Pattern *> &patterns, Where *where,
}
for (auto *pattern : patterns) {
matching.filters.CollectPatternFilters(*pattern, symbol_table, storage);
if (pattern->identifier_->user_declared_) {
std::vector<Symbol> path_elements;
for (auto *pattern_atom : pattern->atoms_)
path_elements.emplace_back(symbol_table.at(*pattern_atom->identifier_));
matching.named_paths.emplace(symbol_table.at(*pattern->identifier_),
std::move(path_elements));
}
}
if (where) {
matching.filters.CollectWhereFilter(*where, symbol_table);
@ -624,7 +631,6 @@ Expression *ExtractFilters(const std::unordered_set<Symbol> &bound_symbols,
}
return filter_expr;
}
} // namespace
namespace impl {
@ -662,6 +668,30 @@ LogicalOperator *GenFilters(LogicalOperator *last_op,
return last_op;
}
LogicalOperator *GenNamedPaths(
LogicalOperator *last_op, std::unordered_set<Symbol> &bound_symbols,
std::unordered_map<Symbol, std::vector<Symbol>> &named_paths) {
auto all_are_bound = [&bound_symbols](const std::vector<Symbol> &syms) {
for (const auto &sym : syms)
if (bound_symbols.find(sym) == bound_symbols.end()) return false;
return true;
};
for (auto named_path_it = named_paths.begin();
named_path_it != named_paths.end();) {
if (all_are_bound(named_path_it->second)) {
last_op = new ConstructNamedPath(
std::shared_ptr<LogicalOperator>(last_op), named_path_it->first,
std::move(named_path_it->second));
bound_symbols.insert(named_path_it->first);
named_path_it = named_paths.erase(named_path_it);
} else {
++named_path_it;
}
}
return last_op;
}
LogicalOperator *GenReturn(Return &ret, LogicalOperator *input_op,
SymbolTable &symbol_table, bool is_write,
const std::unordered_set<Symbol> &bound_symbols,
@ -707,7 +737,20 @@ LogicalOperator *GenCreateForPattern(
input_symbol, node_existing);
};
return ReducePattern<LogicalOperator *>(pattern, base, collect);
LogicalOperator *last_op =
ReducePattern<LogicalOperator *>(pattern, base, collect);
// If the pattern is named, append the path constructing logical operator.
if (pattern.identifier_->user_declared_) {
std::vector<Symbol> path_elements;
for (const PatternAtom *atom : pattern.atoms_)
path_elements.emplace_back(symbol_table.at(*atom->identifier_));
last_op = new ConstructNamedPath(std::shared_ptr<LogicalOperator>(last_op),
symbol_table.at(*pattern.identifier_),
path_elements);
}
return last_op;
}
// Generate an operator for a clause which writes to the database. If the clause

View File

@ -120,6 +120,8 @@ struct Matching {
Filters filters;
/// Maps node symbols to expansions which bind them.
std::unordered_map<Symbol, std::set<int>> node_symbol_to_expansions{};
/// Maps named path symbols to a vector of Symbols that define its pattern.
std::unordered_map<Symbol, std::vector<Symbol>> named_paths{};
/// All node and edge symbols across all expansions (from all matches).
std::unordered_set<Symbol> expansion_symbols{};
};
@ -230,6 +232,14 @@ LogicalOperator *GenFilters(LogicalOperator *last_op,
const std::unordered_set<Symbol> &bound_symbols,
std::vector<Filters::FilterInfo> &all_filters,
AstTreeStorage &storage);
//
/// For all given `named_paths` checks if the all it's symbols have been bound.
/// If so it creates a logical operator for named path generation, binds it's
/// symbol, removes that path from the collection of unhandled ones and returns
/// the new op. Otherwise it returns nullptr.
LogicalOperator *GenNamedPaths(
LogicalOperator *last_op, std::unordered_set<Symbol> &bound_symbols,
std::unordered_map<Symbol, std::vector<Symbol>> &named_paths);
LogicalOperator *GenReturn(Return &ret, LogicalOperator *input_op,
SymbolTable &symbol_table, bool is_write,
@ -474,6 +484,8 @@ class RuleBasedPlanner {
const auto &matching = match_context.matching;
// Copy all_filters, because we will modify the list as we generate Filters.
auto all_filters = matching.filters.all_filters();
// Copy the named_paths for the same reason.
auto named_paths = matching.named_paths;
// Try to generate any filters even before the 1st match operator. This
// optimizes the optional match which filters only on symbols bound in
// regular match.
@ -496,6 +508,9 @@ class RuleBasedPlanner {
match_context.new_symbols.emplace_back(node1_symbol);
last_op =
impl::GenFilters(last_op, bound_symbols, all_filters, storage);
last_op = impl::GenNamedPaths(last_op, bound_symbols, named_paths);
last_op =
impl::GenFilters(last_op, bound_symbols, all_filters, storage);
}
// We have an edge, so generate Expand.
if (expansion.edge) {
@ -598,6 +613,9 @@ class RuleBasedPlanner {
}
last_op =
impl::GenFilters(last_op, bound_symbols, all_filters, storage);
last_op = impl::GenNamedPaths(last_op, bound_symbols, named_paths);
last_op =
impl::GenFilters(last_op, bound_symbols, all_filters, storage);
}
}
debug_assert(all_filters.empty(), "Expected to generate all filters");

View File

@ -421,7 +421,7 @@ TypedValue operator==(const TypedValue &a, const TypedValue &b) {
return true;
}
case TypedValue::Type::Path:
throw utils::NotYetImplemented("equality for TypedValue::Type::Path");
return a.ValuePath() == b.ValuePath();
default:
permanent_fail("Unhandled comparison for types");
}
@ -657,8 +657,10 @@ size_t TypedValue::Hash::operator()(const TypedValue &value) const {
case TypedValue::Type::Edge:
return value.Value<EdgeAccessor>().temporary_id();
case TypedValue::Type::Path:
throw utils::NotYetImplemented("hashing for TypedValue::Type::Path");
break;
return FnvCollection<std::vector<VertexAccessor>, VertexAccessor>{}(
value.ValuePath().vertices()) ^
FnvCollection<std::vector<EdgeAccessor>, EdgeAccessor>{}(
value.ValuePath().edges());
}
permanent_fail("Unhandled TypedValue.type() in hash function");
}

View File

@ -8,6 +8,7 @@
#include <unordered_set>
#include <vector>
#include "query/path.hpp"
#include "storage/edge_accessor.hpp"
#include "storage/property_value.hpp"
#include "storage/vertex_accessor.hpp"
@ -17,8 +18,6 @@
namespace query {
typedef traversal_template::Path<VertexAccessor, EdgeAccessor> Path;
// TODO: Neo4j does overflow checking. Should we also implement it?
/**
* Encapsulation of a value and it's type encapsulated in a class that has no

View File

@ -43,3 +43,11 @@ class EdgeAccessor : public RecordAccessor<Edge> {
};
std::ostream &operator<<(std::ostream &, const EdgeAccessor &);
// hash function for the edge accessor
namespace std {
template <>
struct hash<EdgeAccessor> {
size_t operator()(const EdgeAccessor &e) const { return e.temporary_id(); };
};
}

View File

@ -66,15 +66,20 @@ struct FnvCollection {
static const uint64_t fnv_prime = 1099511628211u;
};
template<typename TA, typename TB>
/**
* Like FNV hashing for a collection, just specialized for two elements to avoid
* iteration overhead.
*/
template <typename TA, typename TB, typename TAHash = std::hash<TA>,
typename TBHash = std::hash<TB>>
struct HashCombine {
size_t operator()(const TA& a, const TB& b) const {
size_t operator()(const TA &a, const TB &b) const {
constexpr size_t fnv_prime = 1099511628211UL;
constexpr size_t fnv_offset = 14695981039346656037UL;
size_t ret = fnv_offset;
ret ^= std::hash<TA>()(a);
ret ^= TAHash()(a);
ret *= fnv_prime;
ret ^= std::hash<TB>()(b);
ret ^= TBHash()(b);
return ret;
}
};

View File

@ -428,6 +428,7 @@ class PlanPrinter : public query::plan::HierarchicalLogicalOperatorVisitor {
}
PRE_VISIT(ExpandBreadthFirst);
PRE_VISIT(ConstructNamedPath);
PRE_VISIT(Filter);
PRE_VISIT(SetProperty);
PRE_VISIT(SetProperties);

View File

@ -484,3 +484,18 @@ Feature: Match
Then the result should be:
| n.a | m.a |
| 1 | 2 |
Scenario: Named path with length function.
Given an empty graph
And having executed:
"""
CREATE (:start)-[:type]->()
"""
When executing query:
"""
MATCH path = (:start) -[*0..1]-> () RETURN size(path)
"""
Then the result should be:
| size(path) |
| 0 |
| 1 |

View File

@ -23,9 +23,11 @@
#pragma once
#include <cstdlib>
#include <map>
#include <string>
#include <utility>
#include <vector>
#include <map>
#include "database/dbms.hpp"
#include "database/graph_db_datatypes.hpp"
@ -168,11 +170,30 @@ auto GetNode(AstTreeStorage &storage, const std::string &name,
return node;
}
/// Generates a randomly chosen (uniformly) string from a population of 10 ** 20
std::string random_string() {
std::string str = "rand_str_";
for (int i = 0; i < 20; i++) str += std::to_string(rand() % 10);
return str;
}
///
/// Create a Pattern with given atoms.
///
auto GetPattern(AstTreeStorage &storage, std::vector<PatternAtom *> atoms) {
auto pattern = storage.Create<Pattern>();
pattern->identifier_ = storage.Create<Identifier>(random_string(), false);
pattern->atoms_.insert(pattern->atoms_.begin(), atoms.begin(), atoms.end());
return pattern;
}
///
/// Create a Pattern with given name and atoms.
///
auto GetPattern(AstTreeStorage &storage, const std::string &name,
std::vector<PatternAtom *> atoms) {
auto pattern = storage.Create<Pattern>();
pattern->identifier_ = storage.Create<Identifier>(name, true);
pattern->atoms_.insert(pattern->atoms_.begin(), atoms.begin(), atoms.end());
return pattern;
}
@ -439,6 +460,8 @@ auto GetMerge(AstTreeStorage &storage, Pattern *pattern, OnMatch on_match,
#define NODE(...) query::test_common::GetNode(storage, __VA_ARGS__)
#define EDGE(...) query::test_common::GetEdge(storage, __VA_ARGS__)
#define PATTERN(...) query::test_common::GetPattern(storage, {__VA_ARGS__})
#define NAMED_PATTERN(name, ...) \
query::test_common::GetPattern(storage, name, {__VA_ARGS__})
#define OPTIONAL_MATCH(...) \
query::test_common::GetWithPatterns(storage.Create<query::Match>(true), \
{__VA_ARGS__})

View File

@ -833,6 +833,16 @@ TEST(ExpressionEvaluator, FunctionSize) {
.Value<int64_t>(),
3);
ASSERT_THROW(EvaluateFunction("SIZE", {5}), QueryRuntimeException);
Dbms dbms;
auto dba = dbms.active();
auto v0 = dba->InsertVertex();
query::Path path(v0);
EXPECT_EQ(EvaluateFunction("SIZE", {path}).ValueInt(), 0);
auto v1 = dba->InsertVertex();
path.Expand(dba->InsertEdge(v0, v1, dba->EdgeType("type")));
path.Expand(v1);
EXPECT_EQ(EvaluateFunction("SIZE", {path}).ValueInt(), 1);
}
TEST(ExpressionEvaluator, FunctionStartNode) {

View File

@ -25,18 +25,30 @@
using namespace query;
using namespace query::plan;
TEST(QueryPlan, MatchReturn) {
class MatchReturnFixture : public testing::Test {
protected:
Dbms dbms;
auto dba = dbms.active();
// add a few nodes to the database
dba->InsertVertex();
dba->InsertVertex();
dba->AdvanceCommand();
std::unique_ptr<GraphDbAccessor> dba = dbms.active();
AstTreeStorage storage;
SymbolTable symbol_table;
void AddVertices(int count) {
for (int i = 0; i < count; i++) dba->InsertVertex();
}
template <typename TResult>
std::vector<TResult> Results(std::shared_ptr<Produce> &op) {
std::vector<TResult> res;
for (const auto &row : CollectProduce(op.get(), symbol_table, *dba))
res.emplace_back(row[0].Value<TResult>());
return res;
}
};
TEST_F(MatchReturnFixture, MatchReturn) {
AddVertices(2);
dba->AdvanceCommand();
auto test_pull_count = [&](GraphView graph_view) {
auto scan_all =
MakeScanAll(storage, symbol_table, "n", nullptr, graph_view);
@ -57,6 +69,27 @@ TEST(QueryPlan, MatchReturn) {
EXPECT_EQ(3, test_pull_count(GraphView::OLD));
}
TEST_F(MatchReturnFixture, MatchReturnPath) {
AddVertices(2);
dba->AdvanceCommand();
auto scan_all = MakeScanAll(storage, symbol_table, "n", nullptr);
Symbol path_sym = symbol_table.CreateSymbol("path", true);
auto make_path = std::make_shared<ConstructNamedPath>(
scan_all.op_, path_sym, std::vector<Symbol>{scan_all.sym_});
auto output = NEXPR("path", IDENT("path"));
symbol_table[*output->expression_] = path_sym;
symbol_table[*output] = symbol_table.CreateSymbol("named_expression_1", true);
auto produce = MakeProduce(make_path, output);
auto results = Results<query::Path>(produce);
ASSERT_EQ(results.size(), 2);
std::vector<query::Path> expected_paths;
for (const auto &v : dba->Vertices(false)) expected_paths.emplace_back(v);
ASSERT_EQ(expected_paths.size(), 2);
EXPECT_TRUE(std::is_permutation(expected_paths.begin(), expected_paths.end(),
results.begin()));
}
TEST(QueryPlan, MatchReturnCartesian) {
Dbms dbms;
auto dba = dbms.active();
@ -218,25 +251,30 @@ TEST(QueryPlan, NodeFilterMultipleLabels) {
EXPECT_EQ(results.size(), 2);
}
TEST(QueryPlan, Expand) {
class ExpandFixture : public testing::Test {
protected:
Dbms dbms;
auto dba = dbms.active();
// make a V-graph (v3)<-[r2]-(v1)-[r1]->(v2)
auto v1 = dba->InsertVertex();
v1.add_label((GraphDbTypes::Label)1);
auto v2 = dba->InsertVertex();
v2.add_label((GraphDbTypes::Label)2);
auto v3 = dba->InsertVertex();
v3.add_label((GraphDbTypes::Label)3);
auto edge_type = dba->EdgeType("Edge");
dba->InsertEdge(v1, v2, edge_type);
dba->InsertEdge(v1, v3, edge_type);
dba->AdvanceCommand();
std::unique_ptr<GraphDbAccessor> dba = dbms.active();
AstTreeStorage storage;
SymbolTable symbol_table;
// make a V-graph (v3)<-[r2]-(v1)-[r1]->(v2)
VertexAccessor v1 = dba->InsertVertex();
VertexAccessor v2 = dba->InsertVertex();
VertexAccessor v3 = dba->InsertVertex();
GraphDbTypes::EdgeType edge_type = dba->EdgeType("Edge");
EdgeAccessor r1 = dba->InsertEdge(v1, v2, edge_type);
EdgeAccessor r2 = dba->InsertEdge(v1, v3, edge_type);
void SetUp() override {
v1.add_label((GraphDbTypes::Label)1);
v2.add_label((GraphDbTypes::Label)2);
v3.add_label((GraphDbTypes::Label)3);
dba->AdvanceCommand();
}
};
TEST_F(ExpandFixture, Expand) {
auto test_expand = [&](EdgeAtom::Direction direction, GraphView graph_view) {
auto n = MakeScanAll(storage, symbol_table, "n");
auto r_m = MakeExpand(storage, symbol_table, n.op_, n.sym_, "r", direction,
@ -274,6 +312,29 @@ TEST(QueryPlan, Expand) {
EXPECT_EQ(8, test_expand(EdgeAtom::Direction::BOTH, GraphView::OLD));
}
TEST_F(ExpandFixture, ExpandPath) {
auto n = MakeScanAll(storage, symbol_table, "n");
auto r_m = MakeExpand(storage, symbol_table, n.op_, n.sym_, "r",
EdgeAtom::Direction::OUT, nullptr, false, "m", false);
Symbol path_sym = symbol_table.CreateSymbol("path", true);
auto path = std::make_shared<ConstructNamedPath>(
r_m.op_, path_sym,
std::vector<Symbol>{n.sym_, r_m.edge_sym_, r_m.node_sym_});
auto output = NEXPR("m", IDENT("m"));
symbol_table[*output->expression_] = path_sym;
symbol_table[*output] = symbol_table.CreateSymbol("named_expression_1", true);
auto produce = MakeProduce(path, output);
std::vector<query::Path> expected_paths{{v1, r2, v3}, {v1, r1, v2}};
auto results = CollectProduce(produce.get(), symbol_table, *dba);
ASSERT_EQ(results.size(), 2);
std::vector<query::Path> results_paths;
for (const auto &result : results)
results_paths.emplace_back(result[0].ValuePath());
EXPECT_TRUE(std::is_permutation(expected_paths.begin(), expected_paths.end(),
results_paths.begin()));
}
/**
* A fixture that sets a graph up and provides some functions.
*
@ -288,7 +349,7 @@ TEST(QueryPlan, Expand) {
*/
class QueryPlanExpandVariable : public testing::Test {
protected:
// type returned by the GetResults function, used
// type returned by the GetEdgeListSizes function, used
// a lot below in test declaration
using map_int = std::unordered_map<int, int>;
@ -296,6 +357,8 @@ class QueryPlanExpandVariable : public testing::Test {
std::unique_ptr<GraphDbAccessor> dba = dbms.active();
// labels for layers in the double chain
std::vector<GraphDbTypes::Label> labels;
// for all the edges
GraphDbTypes::EdgeType edge_type = dba->EdgeType("edge_type");
AstTreeStorage storage;
SymbolTable symbol_table;
@ -318,7 +381,7 @@ class QueryPlanExpandVariable : public testing::Test {
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->InsertEdge(v_from, v_to, dba->EdgeType("edge_type"));
auto edge = dba->InsertEdge(v_from, v_to, edge_type);
edge.PropsSet(dba->Property("p"),
fmt::format("V{}{}->V{}{}", from_layer_ind, v_from_ind,
from_layer_ind + 1, v_to_ind));
@ -385,19 +448,35 @@ class QueryPlanExpandVariable : public testing::Test {
}
/**
* Pulls from the given input and analyses the edge-list (result of variable
* length expansion) found in the results under the given symbol.
* Pulls from the given input and returns the results under the given symbol.
*
* @return a map {path_lenth -> number_of_results}
* @return a vector of values of the given type.
* @tparam TResult type of the result that is sought.
*/
template <typename TResult>
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);
Context context(*dba);
context.symbol_table_ = symbol_table;
while (cursor->Pull(frame, context)) {
auto length = frame[symbol].Value<std::vector<TypedValue>>().size();
std::vector<TResult> results;
while (cursor->Pull(frame, context))
results.emplace_back(frame[symbol].Value<TResult>());
return results;
}
/**
* 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 {edge_list_length -> number_of_results}
*/
auto GetEdgeListSizes(std::shared_ptr<LogicalOperator> input_op,
Symbol symbol) {
map_int count_per_length;
for (const auto &edge_list :
GetResults<std::vector<TypedValue>>(input_op, symbol)) {
auto length = edge_list.size();
auto found = count_per_length.find(length);
if (found == count_per_length.end())
count_per_length[length] = 1;
@ -414,10 +493,11 @@ TEST_F(QueryPlanExpandVariable, OneVariableExpansion) {
std::experimental::optional<size_t> upper,
bool reverse) {
auto e = Edge("r", direction);
return GetResults(AddMatch<ExpandVariable>(nullptr, "n", layer, direction,
nullptr, lower, upper, e, false,
"m", GraphView::AS_IS, reverse),
e);
return GetEdgeListSizes(
AddMatch<ExpandVariable>(nullptr, "n", layer, direction, nullptr, lower,
upper, e, false, "m", GraphView::AS_IS,
reverse),
e);
};
for (int reverse = 0; reverse < 2; ++reverse) {
@ -501,7 +581,7 @@ TEST_F(QueryPlanExpandVariable, EdgeUniquenessSingleAndVariableExpansion) {
last_op, last_symbol, symbols);
}
return GetResults(last_op, var_length_sym);
return GetEdgeListSizes(last_op, var_length_sym);
};
// no uniqueness between variable and single expansion
@ -531,7 +611,7 @@ TEST_F(QueryPlanExpandVariable, EdgeUniquenessTwoVariableExpansions) {
last_op, e2, std::vector<Symbol>{e1});
}
return GetResults(last_op, e2);
return GetEdgeListSizes(last_op, e2);
};
EXPECT_EQ(test_expand(0, EdgeAtom::Direction::OUT, 2, 2, false),
@ -553,7 +633,7 @@ TEST_F(QueryPlanExpandVariable, ExistingEdges) {
auto second =
AddMatch<ExpandVariable>(first, "n2", layer, direction, nullptr, lower,
upper, e2, same_edge_symbol, "m2");
return GetResults(second, e2);
return GetEdgeListSizes(second, e2);
};
EXPECT_EQ(test_expand(0, EdgeAtom::Direction::OUT, 1, 1, false),
@ -580,15 +660,12 @@ TEST_F(QueryPlanExpandVariable, ExistingEdges) {
TEST_F(QueryPlanExpandVariable, GraphState) {
auto test_expand = [&](GraphView graph_view, const auto &edge_type) {
auto e = Edge("r", EdgeAtom::Direction::OUT);
return GetResults(
return GetEdgeListSizes(
AddMatch<ExpandVariable>(nullptr, "n", 0, EdgeAtom::Direction::OUT,
edge_type, 2, 2, e, false, "m", graph_view),
e);
};
EXPECT_EQ(test_expand(GraphView::OLD, dba->EdgeType("edge_type")),
(map_int{{2, 8}}));
auto new_edge_type = dba->EdgeType("some_type");
// add two vertices branching out from the second layer
for (VertexAccessor &vertex : dba->Vertices(true))
@ -602,18 +679,46 @@ TEST_F(QueryPlanExpandVariable, GraphState) {
EXPECT_EQ(test_expand(GraphView::OLD, nullptr), (map_int{{2, 8}}));
EXPECT_EQ(test_expand(GraphView::OLD, new_edge_type), (map_int{}));
EXPECT_EQ(test_expand(GraphView::NEW, nullptr), (map_int{{2, 12}}));
EXPECT_EQ(test_expand(GraphView::NEW, dba->EdgeType("edge_type")),
(map_int{{2, 8}}));
EXPECT_EQ(test_expand(GraphView::NEW, edge_type), (map_int{{2, 8}}));
EXPECT_EQ(test_expand(GraphView::NEW, new_edge_type), (map_int{}));
dba->AdvanceCommand();
for (const auto graph_view : {GraphView::OLD, GraphView::NEW}) {
EXPECT_EQ(test_expand(graph_view, nullptr), (map_int{{2, 12}}));
EXPECT_EQ(test_expand(graph_view, dba->EdgeType("edge_type")),
(map_int{{2, 8}}));
EXPECT_EQ(test_expand(graph_view, edge_type), (map_int{{2, 8}}));
EXPECT_EQ(test_expand(graph_view, new_edge_type), (map_int{}));
}
}
TEST_F(QueryPlanExpandVariable, NamedPath) {
auto e = Edge("r", EdgeAtom::Direction::OUT);
auto expand = AddMatch<ExpandVariable>(
nullptr, "n", 0, EdgeAtom::Direction::OUT, nullptr, 2, 2, e, false, "m");
auto find_symbol = [this](const std::string &name) {
for (const auto &pos_sym : symbol_table.table())
if (pos_sym.second.name() == name) return pos_sym.second;
throw std::runtime_error("Symbol not found");
};
auto path_symbol =
symbol_table.CreateSymbol("path", true, Symbol::Type::Path);
auto create_path = std::make_shared<ConstructNamedPath>(
expand, path_symbol,
std::vector<Symbol>{find_symbol("n"), e, find_symbol("m")});
std::vector<query::Path> expected_paths;
for (const auto &v : dba->Vertices(labels[0], false))
for (const auto &e1 : v.out())
for (const auto &e2 : e1.to().out())
expected_paths.emplace_back(v, e1, e1.to(), e2, e2.to());
ASSERT_EQ(expected_paths.size(), 8);
auto results = GetResults<query::Path>(create_path, path_symbol);
ASSERT_EQ(results.size(), 8);
EXPECT_TRUE(std::is_permutation(results.begin(), results.end(),
expected_paths.begin(),
TypedValue::BoolEqual{}));
}
namespace std {
template <>
struct hash<std::pair<int, int>> {

View File

@ -57,6 +57,7 @@ class PlanChecker : public HierarchicalLogicalOperatorVisitor {
PRE_VISIT(ExpandVariable);
PRE_VISIT(ExpandBreadthFirst);
PRE_VISIT(Filter);
PRE_VISIT(ConstructNamedPath);
PRE_VISIT(Produce);
PRE_VISIT(SetProperty);
PRE_VISIT(SetProperties);
@ -83,7 +84,7 @@ class PlanChecker : public HierarchicalLogicalOperatorVisitor {
PRE_VISIT(Unwind);
PRE_VISIT(Distinct);
bool Visit(Once &op) override {
bool Visit(Once &) override {
// Ignore checking Once, it is implicitly at the end.
return true;
}
@ -115,7 +116,7 @@ class OpChecker : public BaseOpChecker {
ExpectOp(*expected_op, symbol_table);
}
virtual void ExpectOp(TOp &op, const SymbolTable &) {}
virtual void ExpectOp(TOp &, const SymbolTable &) {}
};
using ExpectCreateNode = OpChecker<CreateNode>;
@ -127,6 +128,7 @@ using ExpectExpand = OpChecker<Expand>;
using ExpectExpandVariable = OpChecker<ExpandVariable>;
using ExpectExpandBreadthFirst = OpChecker<ExpandBreadthFirst>;
using ExpectFilter = OpChecker<Filter>;
using ExpectConstructNamedPath = OpChecker<ConstructNamedPath>;
using ExpectProduce = OpChecker<Produce>;
using ExpectSetProperty = OpChecker<SetProperty>;
using ExpectSetProperties = OpChecker<SetProperties>;
@ -147,7 +149,7 @@ class ExpectAccumulate : public OpChecker<Accumulate> {
ExpectAccumulate(const std::unordered_set<Symbol> &symbols)
: symbols_(symbols) {}
void ExpectOp(Accumulate &op, const SymbolTable &symbol_table) override {
void ExpectOp(Accumulate &op, const SymbolTable &) override {
std::unordered_set<Symbol> got_symbols(op.symbols().begin(),
op.symbols().end());
EXPECT_EQ(symbols_, got_symbols);
@ -364,6 +366,18 @@ TEST(TestLogicalPlanner, CreateNodeExpandNode) {
ExpectCreateNode());
}
TEST(TestLogicalPlanner, CreateNamedPattern) {
// Test CREATE p = (n) -[r :rel]-> (m)
AstTreeStorage storage;
Dbms dbms;
auto dba = dbms.active();
auto relationship = dba->EdgeType("rel");
QUERY(CREATE(NAMED_PATTERN(
"p", NODE("n"), EDGE("r", relationship, Direction::OUT), NODE("m"))));
CheckPlan(storage, ExpectCreateNode(), ExpectCreateExpand(),
ExpectConstructNamedPath());
}
TEST(TestLogicalPlanner, MatchCreateExpand) {
// Test MATCH (n) CREATE (n) -[r :rel1]-> (m)
AstTreeStorage storage;
@ -398,6 +412,32 @@ TEST(TestLogicalPlanner, MatchPathReturn) {
ExpectProduce());
}
TEST(TestLogicalPlanner, MatchNamedPatternReturn) {
// Test MATCH p = (n) -[r :relationship]- (m) RETURN p
AstTreeStorage storage;
Dbms dbms;
auto dba = dbms.active();
auto relationship = dba->EdgeType("relationship");
QUERY(
MATCH(NAMED_PATTERN("p", NODE("n"), EDGE("r", relationship), NODE("m"))),
RETURN("n"));
CheckPlan(storage, ExpectScanAll(), ExpectExpand(), ExpectFilter(),
ExpectConstructNamedPath(), ExpectProduce());
}
TEST(TestLogicalPlanner, MatchNamedPatternWithPredicateReturn) {
// Test MATCH p = (n) -[r :relationship]- (m) RETURN p
AstTreeStorage storage;
Dbms dbms;
auto dba = dbms.active();
auto relationship = dba->EdgeType("relationship");
QUERY(
MATCH(NAMED_PATTERN("p", NODE("n"), EDGE("r", relationship), NODE("m"))),
WHERE(EQ(LITERAL(2), IDENT("p"))), RETURN("n"));
CheckPlan(storage, ExpectScanAll(), ExpectExpand(), ExpectFilter(),
ExpectConstructNamedPath(), ExpectFilter(), ExpectProduce());
}
TEST(TestLogicalPlanner, MatchWhereReturn) {
// Test MATCH (n) WHERE n.property < 42 RETURN n
AstTreeStorage storage;

View File

@ -21,9 +21,13 @@ TEST(TestSymbolGenerator, MatchNodeReturn) {
QUERY(MATCH(PATTERN(NODE("node_atom_1"))), RETURN("node_atom_1"));
SymbolGenerator symbol_generator(symbol_table);
query_ast->Accept(symbol_generator);
EXPECT_EQ(symbol_table.max_position(), 2);
// symbols for pattern, node_atom_1 and named_expr in return
EXPECT_EQ(symbol_table.max_position(), 3);
auto match = dynamic_cast<Match *>(query_ast->clauses_[0]);
auto pattern = match->patterns_[0];
auto pattern_sym = symbol_table[*pattern->identifier_];
EXPECT_EQ(pattern_sym.type(), Symbol::Type::Path);
EXPECT_FALSE(pattern_sym.user_declared());
auto node_atom = dynamic_cast<NodeAtom *>(pattern->atoms_[0]);
auto node_sym = symbol_table[*node_atom->identifier_];
EXPECT_EQ(node_sym.name(), "node_atom_1");
@ -37,6 +41,24 @@ TEST(TestSymbolGenerator, MatchNodeReturn) {
EXPECT_EQ(node_sym, ret_sym);
}
TEST(TestSymbolGenerator, MatchNamedPattern) {
SymbolTable symbol_table;
AstTreeStorage storage;
// MATCH p = (node_atom_1) RETURN node_atom_1
auto query_ast = QUERY(MATCH(NAMED_PATTERN("p", NODE("node_atom_1"))),
RETURN("p"));
SymbolGenerator symbol_generator(symbol_table);
query_ast->Accept(symbol_generator);
// symbols for p, node_atom_1 and named_expr in return
EXPECT_EQ(symbol_table.max_position(), 3);
auto match = dynamic_cast<Match *>(query_ast->clauses_[0]);
auto pattern = match->patterns_[0];
auto pattern_sym = symbol_table[*pattern->identifier_];
EXPECT_EQ(pattern_sym.type(), Symbol::Type::Path);
EXPECT_EQ(pattern_sym.name(), "p");
EXPECT_TRUE(pattern_sym.user_declared());
}
TEST(TestSymbolGenerator, MatchUnboundMultiReturn) {
SymbolTable symbol_table;
AstTreeStorage storage;
@ -69,7 +91,8 @@ TEST(TestSymbolGenerator, MatchSameEdge) {
RETURN("r"));
SymbolGenerator symbol_generator(symbol_table);
query_ast->Accept(symbol_generator);
EXPECT_EQ(symbol_table.max_position(), 3);
// symbols for pattern, `n`, `r` and named_expr in return
EXPECT_EQ(symbol_table.max_position(), 4);
auto match = dynamic_cast<Match *>(query_ast->clauses_[0]);
auto pattern = match->patterns_[0];
std::vector<Symbol> node_symbols;
@ -120,7 +143,8 @@ TEST(TestSymbolGenerator, CreateNodeReturn) {
auto query_ast = QUERY(CREATE(PATTERN(NODE("n"))), RETURN("n"));
SymbolGenerator symbol_generator(symbol_table);
query_ast->Accept(symbol_generator);
EXPECT_EQ(symbol_table.max_position(), 2);
// symbols for pattern, `n` and named_expr
EXPECT_EQ(symbol_table.max_position(), 3);
auto create = dynamic_cast<Create *>(query_ast->clauses_[0]);
auto pattern = create->patterns_[0];
auto node_atom = dynamic_cast<NodeAtom *>(pattern->atoms_[0]);
@ -252,7 +276,8 @@ TEST(TestSymbolGenerator, CreateDelete) {
SymbolTable symbol_table;
SymbolGenerator symbol_generator(symbol_table);
query->Accept(symbol_generator);
EXPECT_EQ(symbol_table.max_position(), 1);
// symbols for pattern and `n`
EXPECT_EQ(symbol_table.max_position(), 2);
auto node_symbol = symbol_table.at(*node->identifier_);
auto ident_symbol = symbol_table.at(*ident);
EXPECT_EQ(node_symbol.type(), Symbol::Type::Vertex);
@ -281,7 +306,8 @@ TEST(TestSymbolGenerator, MatchWithReturn) {
SymbolTable symbol_table;
SymbolGenerator symbol_generator(symbol_table);
query->Accept(symbol_generator);
EXPECT_EQ(symbol_table.max_position(), 3);
// symbols for pattern, `old`, `n` and named_expr in return
EXPECT_EQ(symbol_table.max_position(), 4);
auto node_symbol = symbol_table.at(*node->identifier_);
auto old = symbol_table.at(*old_ident);
EXPECT_EQ(node_symbol, old);
@ -318,7 +344,8 @@ TEST(TestSymbolGenerator, MatchWithWhere) {
SymbolTable symbol_table;
SymbolGenerator symbol_generator(symbol_table);
query->Accept(symbol_generator);
EXPECT_EQ(symbol_table.max_position(), 2);
// symbols for pattern, `old` and `n`
EXPECT_EQ(symbol_table.max_position(), 3);
auto node_symbol = symbol_table.at(*node->identifier_);
auto old = symbol_table.at(*old_ident);
EXPECT_EQ(node_symbol, old);
@ -360,7 +387,8 @@ TEST(TestSymbolGenerator, CreateMultiExpand) {
SymbolTable symbol_table;
SymbolGenerator symbol_generator(symbol_table);
query->Accept(symbol_generator);
EXPECT_EQ(symbol_table.max_position(), 5);
// symbols for pattern * 2, `n`, `r`, `m`, `p`, `l`
EXPECT_EQ(symbol_table.max_position(), 7);
auto n1 = symbol_table.at(*node_n1->identifier_);
auto n2 = symbol_table.at(*node_n2->identifier_);
EXPECT_EQ(n1, n2);
@ -424,8 +452,8 @@ TEST(TestSymbolGenerator, MatchReturnSum) {
SymbolTable symbol_table;
SymbolGenerator symbol_generator(symbol_table);
query->Accept(symbol_generator);
// 3 symbols for: 'n', 'sum' and 'result'.
EXPECT_EQ(symbol_table.max_position(), 3);
// 3 symbols for: pattern, 'n', 'sum' and 'result'.
EXPECT_EQ(symbol_table.max_position(), 4);
auto node_symbol = symbol_table.at(*node->identifier_);
auto sum_symbol = symbol_table.at(*sum);
EXPECT_NE(node_symbol, sum_symbol);
@ -476,7 +504,8 @@ TEST(TestSymbolGenerator, MatchPropCreateNodeProp) {
SymbolTable symbol_table;
SymbolGenerator symbol_generator(symbol_table);
query->Accept(symbol_generator);
EXPECT_EQ(symbol_table.max_position(), 2);
// symbols: pattern * 2, `node_n`, `node_m`
EXPECT_EQ(symbol_table.max_position(), 4);
auto n = symbol_table.at(*node_n->identifier_);
EXPECT_EQ(n, symbol_table.at(*n_prop->expression_));
auto m = symbol_table.at(*node_m->identifier_);
@ -497,7 +526,8 @@ TEST(TestSymbolGenerator, CreateNodeEdge) {
SymbolTable symbol_table;
SymbolGenerator symbol_generator(symbol_table);
query->Accept(symbol_generator);
EXPECT_EQ(symbol_table.max_position(), 2);
// symbols: pattern * 2, `n`, `r`
EXPECT_EQ(symbol_table.max_position(), 4);
auto n = symbol_table.at(*node_1->identifier_);
EXPECT_EQ(n, symbol_table.at(*node_2->identifier_));
EXPECT_EQ(n, symbol_table.at(*node_3->identifier_));
@ -519,7 +549,8 @@ TEST(TestSymbolGenerator, MatchWithCreate) {
SymbolTable symbol_table;
SymbolGenerator symbol_generator(symbol_table);
query->Accept(symbol_generator);
EXPECT_EQ(symbol_table.max_position(), 3);
// symbols: pattern * 2, `n`, `m`, `r`
EXPECT_EQ(symbol_table.max_position(), 5);
auto n = symbol_table.at(*node_1->identifier_);
EXPECT_EQ(n.type(), Symbol::Type::Vertex);
auto m = symbol_table.at(*node_2->identifier_);
@ -612,8 +643,8 @@ TEST(TestSymbolGenerator, AggregationOrderBy) {
SymbolTable symbol_table;
SymbolGenerator symbol_generator(symbol_table);
query->Accept(symbol_generator);
// Symbols for `old`, `count(old)` and `new`
EXPECT_EQ(symbol_table.max_position(), 3);
// Symbols for pattern, `old`, `count(old)` and `new`
EXPECT_EQ(symbol_table.max_position(), 4);
auto old = symbol_table.at(*node->identifier_);
EXPECT_EQ(old, symbol_table.at(*ident_old));
auto new_sym = symbol_table.at(*as_new);
@ -633,8 +664,8 @@ TEST(TestSymbolGenerator, OrderByOldVariable) {
SymbolTable symbol_table;
SymbolGenerator symbol_generator(symbol_table);
query->Accept(symbol_generator);
// Symbols for `old` and `new`
EXPECT_EQ(symbol_table.max_position(), 2);
// Symbols for pattern, `old` and `new`
EXPECT_EQ(symbol_table.max_position(), 3);
auto old = symbol_table.at(*node->identifier_);
EXPECT_EQ(old, symbol_table.at(*ident_old));
EXPECT_EQ(old, symbol_table.at(*by_old));
@ -699,8 +730,8 @@ TEST(TestSymbolGenerator, MergeOnMatchOnCreate) {
SymbolTable symbol_table;
SymbolGenerator symbol_generator(symbol_table);
query->Accept(symbol_generator);
// Symbols for: `n`, `r`, `m` and `AS r`.
EXPECT_EQ(symbol_table.max_position(), 4);
// Symbols for: pattern * 2, `n`, `r`, `m` and `AS r`.
EXPECT_EQ(symbol_table.max_position(), 6);
auto n = symbol_table.at(*match_n->identifier_);
EXPECT_EQ(n, symbol_table.at(*merge_n->identifier_));
EXPECT_EQ(n, symbol_table.at(*n_prop->expression_));
@ -770,8 +801,8 @@ TEST(TestSymbolGenerator, MatchCrossReferenceVariable) {
SymbolTable symbol_table;
SymbolGenerator symbol_generator(symbol_table);
query->Accept(symbol_generator);
// Symbols for `n`, `m` and `AS n`
EXPECT_EQ(symbol_table.max_position(), 3);
// Symbols for pattern * 2, `n`, `m` and `AS n`
EXPECT_EQ(symbol_table.max_position(), 5);
auto n = symbol_table.at(*node_n->identifier_);
EXPECT_EQ(n, symbol_table.at(*n_prop->expression_));
EXPECT_EQ(n, symbol_table.at(*ident_n));
@ -800,8 +831,8 @@ TEST(TestSymbolGenerator, MatchWithAsteriskReturnAsterisk) {
SymbolTable symbol_table;
SymbolGenerator symbol_generator(symbol_table);
query->Accept(symbol_generator);
// Symbols for `n`, `e`, `m`, `AS n.prop`.
EXPECT_EQ(symbol_table.max_position(), 4);
// Symbols for pattern, `n`, `e`, `m`, `AS n.prop`.
EXPECT_EQ(symbol_table.max_position(), 5);
auto n = symbol_table.at(*node_n->identifier_);
EXPECT_EQ(n, symbol_table.at(*n_prop->expression_));
}
@ -860,8 +891,8 @@ TEST(TestSymbolGenerator, MatchEdgeWithIdentifierInProperty) {
SymbolTable symbol_table;
SymbolGenerator symbol_generator(symbol_table);
query->Accept(symbol_generator);
// Symbols for `n`, `r`, `m` and implicit in RETURN `r AS r`
EXPECT_EQ(symbol_table.max_position(), 4);
// Symbols for pattern, `n`, `r`, `m` and implicit in RETURN `r AS r`
EXPECT_EQ(symbol_table.max_position(), 5);
auto n = symbol_table.at(*node_n->identifier_);
EXPECT_EQ(n, symbol_table.at(*n_prop->expression_));
}
@ -882,8 +913,8 @@ TEST(TestSymbolGenerator, MatchVariablePathUsingIdentifier) {
SymbolTable symbol_table;
SymbolGenerator symbol_generator(symbol_table);
query->Accept(symbol_generator);
// Symbols for `n`, `r`, `m`, `l` and implicit in RETURN `r AS r`
EXPECT_EQ(symbol_table.max_position(), 5);
// Symbols for pattern * 2, `n`, `r`, `m`, `l` and implicit in RETURN `r AS r`
EXPECT_EQ(symbol_table.max_position(), 7);
auto l = symbol_table.at(*node_l->identifier_);
EXPECT_EQ(l, symbol_table.at(*l_prop->expression_));
auto r = symbol_table.at(*edge->identifier_);
@ -1027,8 +1058,8 @@ TEST(TestSymbolGenerator, MatchBfsReturn) {
SymbolTable symbol_table;
SymbolGenerator symbol_generator(symbol_table);
query->Accept(symbol_generator);
// Symbols for `n`, `[r]`, `r|`, `n|`, `m` and `AS r`.
EXPECT_EQ(symbol_table.max_position(), 6);
// Symbols for pattern, `n`, `[r]`, `r|`, `n|`, `m` and `AS r`.
EXPECT_EQ(symbol_table.max_position(), 7);
EXPECT_EQ(symbol_table.at(*ret_r), symbol_table.at(*bfs->identifier_));
EXPECT_NE(symbol_table.at(*ret_r),
symbol_table.at(*bfs->traversed_edge_identifier_));

View File

@ -38,7 +38,7 @@ class AllTypesFixture : public testing::Test {
values_.emplace_back(vertex);
values_.emplace_back(
dba_->InsertEdge(vertex, vertex, dba_->EdgeType("et")));
values_.emplace_back(query::Path{});
values_.emplace_back(query::Path(dba_->InsertVertex()));
}
};