Merge branch 'master' into add-bug-tracking-workflow

This commit is contained in:
Jure Bajic 2023-03-14 09:58:28 +01:00 committed by GitHub
commit d371fefc86
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
59 changed files with 5055 additions and 273 deletions

343
include/_mgp_mock.py Normal file
View File

@ -0,0 +1,343 @@
import typing
from enum import Enum
import networkx as nx
NX_LABEL_ATTR = "labels"
NX_TYPE_ATTR = "type"
SOURCE_TYPE_KAFKA = "SOURCE_TYPE_KAFKA"
SOURCE_TYPE_PULSAR = "SOURCE_TYPE_PULSAR"
"""
This module provides helpers for the mock Python API, much like _mgp.py does for mgp.py.
"""
class InvalidArgumentError(Exception):
"""
Signals that some of the arguments have invalid values.
"""
pass
class ImmutableObjectError(Exception):
pass
class LogicErrorError(Exception):
pass
class DeletedObjectError(Exception):
pass
class EdgeConstants(Enum):
I_START = 0
I_END = 1
I_KEY = 2
class Graph:
"""Wrapper around a NetworkX MultiDiGraph instance."""
__slots__ = ("nx", "_highest_vertex_id", "_highest_edge_id", "_valid")
def __init__(self, graph: nx.MultiDiGraph) -> None:
if not isinstance(graph, nx.MultiDiGraph):
raise TypeError(f"Expected 'networkx.classes.multidigraph.MultiDiGraph', got '{type(graph)}'")
self.nx = graph
self._highest_vertex_id = None
self._highest_edge_id = None
self._valid = True
@property
def vertex_ids(self):
return self.nx.nodes
def vertex_is_isolate(self, vertex_id: int) -> bool:
return nx.is_isolate(self.nx, vertex_id)
@property
def vertices(self):
return (Vertex(node_id, self) for node_id in self.nx.nodes)
def has_node(self, node_id):
return self.nx.has_node(node_id)
@property
def edges(self):
return self.nx.edges
def is_valid(self) -> bool:
return self._valid
def get_vertex_by_id(self, vertex_id: int) -> "Vertex":
return Vertex(vertex_id, self)
def invalidate(self):
self._valid = False
def is_immutable(self) -> bool:
return nx.is_frozen(self.nx)
def make_immutable(self):
self.nx = nx.freeze(self.nx)
def _new_vertex_id(self):
if self._highest_vertex_id is None:
self._highest_vertex_id = max(vertex_id for vertex_id in self.nx.nodes)
return self._highest_vertex_id + 1
def _new_edge_id(self):
if self._highest_edge_id is None:
self._highest_edge_id = max(edge[EdgeConstants.I_KEY.value] for edge in self.nx.edges(keys=True))
return self._highest_edge_id + 1
def create_vertex(self) -> "Vertex":
vertex_id = self._new_vertex_id()
self.nx.add_node(vertex_id)
self._highest_vertex_id = vertex_id
return Vertex(vertex_id, self)
def create_edge(self, from_vertex: "Vertex", to_vertex: "Vertex", edge_type: str) -> "Edge":
if from_vertex.is_deleted() or to_vertex.is_deleted():
raise DeletedObjectError("Accessing deleted object.")
edge_id = self._new_edge_id()
from_id = from_vertex.id
to_id = to_vertex.id
self.nx.add_edge(from_id, to_id, key=edge_id, type=edge_type)
self._highest_edge_id = edge_id
return Edge((from_id, to_id, edge_id), self)
def delete_vertex(self, vertex_id: int):
self.nx.remove_node(vertex_id)
def delete_edge(self, from_vertex_id: int, to_vertex_id: int, edge_id: int):
self.nx.remove_edge(from_vertex_id, to_vertex_id, edge_id)
@property
def highest_vertex_id(self) -> int:
if self._highest_vertex_id is None:
self._highest_vertex_id = max(vertex_id for vertex_id in self.nx.nodes) + 1
return self._highest_vertex_id
@property
def highest_edge_id(self) -> int:
if self._highest_edge_id is None:
self._highest_edge_id = max(edge[EdgeConstants.I_KEY.value] for edge in self.nx.edges(keys=True))
return self._highest_edge_id + 1
class Vertex:
"""Represents a graph vertex."""
__slots__ = ("_id", "_graph")
def __init__(self, id: int, graph: Graph) -> None:
if not isinstance(id, int):
raise TypeError(f"Expected 'int', got '{type(id)}'")
if not isinstance(graph, Graph):
raise TypeError(f"Expected '_mgp_mock.Graph', got '{type(graph)}'")
if not graph.nx.has_node(id):
raise IndexError(f"Unable to find vertex with ID {id}.")
self._id = id
self._graph = graph
def is_valid(self) -> bool:
return self._graph.is_valid()
def is_deleted(self) -> bool:
return not self._graph.nx.has_node(self._id) and self._id <= self._graph.highest_vertex_id
@property
def underlying_graph(self) -> Graph:
return self._graph
def underlying_graph_is_mutable(self) -> bool:
return not nx.is_frozen(self._graph.nx)
@property
def labels(self) -> typing.List[int]:
return self._graph.nx.nodes[self._id][NX_LABEL_ATTR].split(":")
def add_label(self, label: str) -> None:
if nx.is_frozen(self._graph.nx):
raise ImmutableObjectError("Cannot modify immutable object.")
self._graph.nx.nodes[self._id][NX_LABEL_ATTR] += f":{label}"
def remove_label(self, label: str) -> None:
if nx.is_frozen(self._graph.nx):
raise ImmutableObjectError("Cannot modify immutable object.")
labels = self._graph.nx.nodes[self._id][NX_LABEL_ATTR]
if labels.startswith(f"{label}:"):
labels = "\n" + labels # pseudo-string starter
self._graph.nx.nodes[self._id][NX_LABEL_ATTR] = labels.replace(f"\n{label}:", "")
elif labels.endswith(f":{label}"):
labels += "\n" # pseudo-string terminator
self._graph.nx.nodes[self._id][NX_LABEL_ATTR] = labels.replace(f":{label}\n", "")
else:
self._graph.nx.nodes[self._id][NX_LABEL_ATTR] = labels.replace(f":{label}:", ":")
@property
def id(self) -> int:
return self._id
@property
def properties(self):
return (
(key, value)
for key, value in self._graph.nx.nodes[self._id].items()
if key not in (NX_LABEL_ATTR, NX_TYPE_ATTR)
)
def get_property(self, property_name: str):
return self._graph.nx.nodes[self._id][property_name]
def set_property(self, property_name: str, value: object):
self._graph.nx.nodes[self._id][property_name] = value
@property
def in_edges(self) -> typing.Iterable["Edge"]:
return [Edge(edge, self._graph) for edge in self._graph.nx.in_edges(self._id, keys=True)]
@property
def out_edges(self) -> typing.Iterable["Edge"]:
return [Edge(edge, self._graph) for edge in self._graph.nx.out_edges(self._id, keys=True)]
class Edge:
"""Represents a graph edge."""
__slots__ = ("_edge", "_graph")
def __init__(self, edge: typing.Tuple[int, int, int], graph: Graph) -> None:
if not isinstance(edge, typing.Tuple):
raise TypeError(f"Expected 'Tuple', got '{type(edge)}'")
if not isinstance(graph, Graph):
raise TypeError(f"Expected '_mgp_mock.Graph', got '{type(graph)}'")
if not graph.nx.has_edge(*edge):
raise IndexError(f"Unable to find edge with ID {edge[EdgeConstants.I_KEY.value]}.")
self._edge = edge
self._graph = graph
def is_valid(self) -> bool:
return self._graph.is_valid()
def is_deleted(self) -> bool:
return (
not self._graph.nx.has_edge(*self._edge)
and self._edge[EdgeConstants.I_KEY.value] <= self._graph.highest_edge_id
)
def underlying_graph_is_mutable(self) -> bool:
return not nx.is_frozen(self._graph.nx)
@property
def id(self) -> int:
return self._edge[EdgeConstants.I_KEY.value]
@property
def edge(self) -> typing.Tuple[int, int, int]:
return self._edge
@property
def start_id(self) -> int:
return self._edge[EdgeConstants.I_START.value]
@property
def end_id(self) -> int:
return self._edge[EdgeConstants.I_END.value]
def get_type_name(self):
return self._graph.nx.get_edge_data(*self._edge)[NX_TYPE_ATTR]
def from_vertex(self) -> Vertex:
return Vertex(self.start_id, self._graph)
def to_vertex(self) -> Vertex:
return Vertex(self.end_id, self._graph)
@property
def properties(self):
return (
(key, value)
for key, value in self._graph.nx.edges[self._edge].items()
if key not in (NX_LABEL_ATTR, NX_TYPE_ATTR)
)
def get_property(self, property_name: str):
return self._graph.nx.edges[self._edge][property_name]
def set_property(self, property_name: str, value: object):
self._graph.nx.edges[self._edge][property_name] = value
class Path:
"""Represents a path comprised of `Vertex` and `Edge` instances."""
__slots__ = ("_vertices", "_edges", "_graph")
__create_key = object()
def __init__(self, create_key, vertex_id: int, graph: Graph) -> None:
assert create_key == Path.__create_key, "Path objects must be created using Path.make_with_start"
self._vertices = [vertex_id]
self._edges = []
self._graph = graph
@classmethod
def make_with_start(cls, vertex: Vertex) -> "Path":
if not isinstance(vertex, Vertex):
raise TypeError(f"Expected 'Vertex', got '{type(vertex)}'")
if not isinstance(vertex.underlying_graph, Graph):
raise TypeError(f"Expected '_mgp_mock.Graph', got '{type(vertex.underlying_graph)}'")
if not vertex.underlying_graph.nx.has_node(vertex._id):
raise IndexError(f"Unable to find vertex with ID {vertex._id}.")
return Path(cls.__create_key, vertex._id, vertex.underlying_graph)
def is_valid(self) -> bool:
return self._graph.is_valid()
def underlying_graph_is_mutable(self) -> bool:
return not nx.is_frozen(self._graph.nx)
def expand(self, edge: Edge):
if edge.start_id != self._vertices[-1]:
raise LogicErrorError("Logic error.")
self._vertices.append(edge.end_id)
self._edges.append((edge.start_id, edge.end_id, edge.id))
def vertex_at(self, index: int) -> Vertex:
return Vertex(self._vertices[index], self._graph)
def edge_at(self, index: int) -> Edge:
return Edge(self._edges[index], self._graph)
def size(self) -> int:
return len(self._edges)

1655
include/mgp_mock.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -36,7 +36,7 @@ ADDITIONAL USE GRANT: You may use the Licensed Work in accordance with the
3. using the Licensed Work to create a work or solution
which competes (or might reasonably be expected to
compete) with the Licensed Work.
CHANGE DATE: 2027-26-01
CHANGE DATE: 2027-08-03
CHANGE LICENSE: Apache License, Version 2.0
For information about alternative licensing arrangements, please visit: https://memgraph.com/legal.

View File

@ -106,6 +106,10 @@ install(PROGRAMS $<TARGET_FILE:memgraph>
# Install Python source for supporting our embedded Python.
install(FILES ${CMAKE_SOURCE_DIR}/include/mgp.py
DESTINATION lib/memgraph/python_support)
install(FILES ${CMAKE_SOURCE_DIR}/include/mgp_mock.py
DESTINATION lib/memgraph/python_support)
install(FILES ${CMAKE_SOURCE_DIR}/include/_mgp_mock.py
DESTINATION lib/memgraph/python_support)
# Install the includes file for writing custom procedures in C and C++>
install(FILES ${CMAKE_SOURCE_DIR}/include/mg_procedure.h

View File

@ -2711,5 +2711,41 @@ cpp<#
(:serialize (:slk))
(:clone))
(lcp:define-class exists (expression)
((pattern "Pattern *" :initval "nullptr" :scope :public
:slk-save #'slk-save-ast-pointer
:slk-load (slk-load-ast-pointer "Pattern"))
(symbol-pos :int32_t :initval -1 :scope :public
:documentation "Symbol table position of the symbol this Aggregation is mapped to."))
(:public
#>cpp
Exists() = default;
DEFVISITABLE(ExpressionVisitor<TypedValue>);
DEFVISITABLE(ExpressionVisitor<TypedValue*>);
DEFVISITABLE(ExpressionVisitor<void>);
bool Accept(HierarchicalTreeVisitor &visitor) override {
if (visitor.PreVisit(*this)) {
pattern_->Accept(visitor);
}
return visitor.PostVisit(*this);
}
Exists *MapTo(const Symbol &symbol) {
symbol_pos_ = symbol.position();
return this;
}
cpp<#)
(:protected
#>cpp
Exists(Pattern * pattern) : pattern_(pattern) {}
cpp<#)
(:private
#>cpp
friend class AstStorage;
cpp<#)
(:serialize (:slk))
(:clone))
(lcp:pop-namespace) ;; namespace query
(lcp:pop-namespace) ;; namespace memgraph

View File

@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd.
// Copyright 2023 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@ -95,6 +95,7 @@ class SettingQuery;
class VersionQuery;
class Foreach;
class ShowConfigQuery;
class Exists;
using TreeCompositeVisitor = utils::CompositeVisitor<
SingleQuery, CypherUnion, NamedExpression, OrOperator, XorOperator, AndOperator, NotOperator, AdditionOperator,
@ -103,7 +104,7 @@ using TreeCompositeVisitor = utils::CompositeVisitor<
ListSlicingOperator, IfOperator, UnaryPlusOperator, UnaryMinusOperator, IsNullOperator, ListLiteral, MapLiteral,
PropertyLookup, LabelsTest, Aggregation, Function, Reduce, Coalesce, Extract, All, Single, Any, None, CallProcedure,
Create, Match, Return, With, Pattern, NodeAtom, EdgeAtom, Delete, Where, SetProperty, SetProperties, SetLabels,
RemoveProperty, RemoveLabels, Merge, Unwind, RegexMatch, LoadCsv, Foreach>;
RemoveProperty, RemoveLabels, Merge, Unwind, RegexMatch, LoadCsv, Foreach, Exists>;
using TreeLeafVisitor = utils::LeafVisitor<Identifier, PrimitiveLiteral, ParameterLookup>;
@ -123,7 +124,7 @@ class ExpressionVisitor
LessOperator, GreaterOperator, LessEqualOperator, GreaterEqualOperator, InListOperator, SubscriptOperator,
ListSlicingOperator, IfOperator, UnaryPlusOperator, UnaryMinusOperator, IsNullOperator, ListLiteral,
MapLiteral, PropertyLookup, LabelsTest, Aggregation, Function, Reduce, Coalesce, Extract, All, Single, Any,
None, ParameterLookup, Identifier, PrimitiveLiteral, RegexMatch> {};
None, ParameterLookup, Identifier, PrimitiveLiteral, RegexMatch, Exists> {};
template <class TResult>
class QueryVisitor : public utils::Visitor<TResult, CypherQuery, ExplainQuery, ProfileQuery, IndexQuery, AuthQuery,

View File

@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd.
// Copyright 2023 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@ -2160,7 +2160,10 @@ antlrcpp::Any CypherMainVisitor::visitAtom(MemgraphCypher::AtomContext *ctx) {
auto *list = std::any_cast<Expression *>(ctx->extractExpression()->idInColl()->expression()->accept(this));
auto *expr = std::any_cast<Expression *>(ctx->extractExpression()->expression()->accept(this));
return static_cast<Expression *>(storage_->Create<Extract>(ident, list, expr));
} else if (ctx->existsExpression()) {
return std::any_cast<Expression *>(ctx->existsExpression()->accept(this));
}
// TODO: Implement this. We don't support comprehensions, filtering... at
// the moment.
throw utils::NotYetImplemented("atom expression '{}'", ctx->getText());
@ -2204,6 +2207,17 @@ antlrcpp::Any CypherMainVisitor::visitLiteral(MemgraphCypher::LiteralContext *ct
return visitChildren(ctx);
}
antlrcpp::Any CypherMainVisitor::visitExistsExpression(MemgraphCypher::ExistsExpressionContext *ctx) {
auto *exists = storage_->Create<Exists>();
exists->pattern_ = std::any_cast<Pattern *>(ctx->patternPart()->accept(this));
if (exists->pattern_->identifier_) {
throw SyntaxException("Identifiers are not supported in exists(...).");
}
return static_cast<Expression *>(exists);
}
antlrcpp::Any CypherMainVisitor::visitParenthesizedExpression(MemgraphCypher::ParenthesizedExpressionContext *ctx) {
return std::any_cast<Expression *>(ctx->expression()->accept(this));
}

View File

@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd.
// Copyright 2023 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@ -763,6 +763,11 @@ class CypherMainVisitor : public antlropencypher::MemgraphCypherBaseVisitor {
*/
antlrcpp::Any visitParameter(MemgraphCypher::ParameterContext *ctx) override;
/**
* @return Exists* (Expression)
*/
antlrcpp::Any visitExistsExpression(MemgraphCypher::ExistsExpressionContext *ctx) override;
/**
* @return Expression*
*/

View File

@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd.
// Copyright 2023 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@ -60,6 +60,7 @@ class ExpressionPrettyPrinter : public ExpressionVisitor<void> {
void Visit(Reduce &op) override;
void Visit(Coalesce &op) override;
void Visit(Extract &op) override;
void Visit(Exists &op) override;
void Visit(All &op) override;
void Visit(Single &op) override;
void Visit(Any &op) override;
@ -264,6 +265,8 @@ void ExpressionPrettyPrinter::Visit(Extract &op) {
PrintOperator(out_, "Extract", op.identifier_, op.list_, op.expression_);
}
void ExpressionPrettyPrinter::Visit(Exists & /*op*/) { PrintOperator(out_, "Exists", "expression"); }
void ExpressionPrettyPrinter::Visit(All &op) {
PrintOperator(out_, "All", op.identifier_, op.list_expression_, op.where_->expression_);
}

View File

@ -236,6 +236,7 @@ atom : literal
| ( ANY '(' filterExpression ')' )
| ( NONE '(' filterExpression ')' )
| ( SINGLE '(' filterExpression ')' )
| ( EXISTS '(' existsExpression ')' )
| relationshipsPattern
| parenthesizedExpression
| functionInvocation
@ -275,6 +276,8 @@ reduceExpression : accumulator=variable '=' initial=expression ',' idInColl '|'
extractExpression : idInColl '|' expression ;
existsExpression : patternPart ;
idInColl : variable IN expression ;
functionInvocation : functionName '(' ( DISTINCT )? ( expression ( ',' expression )* )? ')' ;

View File

@ -64,6 +64,11 @@ auto SymbolGenerator::CreateSymbol(const std::string &name, bool user_declared,
return symbol;
}
auto SymbolGenerator::CreateAnonymousSymbol(Symbol::Type /*type*/) {
auto symbol = symbol_table_->CreateAnonymousSymbol();
return symbol;
}
auto SymbolGenerator::GetOrCreateSymbol(const std::string &name, bool user_declared, Symbol::Type type) {
// NOLINTNEXTLINE
for (auto scope = scopes_.rbegin(); scope != scopes_.rend(); ++scope) {
@ -302,6 +307,14 @@ SymbolGenerator::ReturnType SymbolGenerator::Visit(Identifier &ident) {
if (scope.in_skip || scope.in_limit) {
throw SemanticException("Variables are not allowed in {}.", scope.in_skip ? "SKIP" : "LIMIT");
}
if (scope.in_exists && (scope.visiting_edge || scope.in_node_atom)) {
auto has_symbol = HasSymbol(ident.name_);
if (!has_symbol && !ConsumePredefinedIdentifier(ident.name_) && ident.user_declared_) {
throw SemanticException("Unbounded variables are not allowed in exists!");
}
}
Symbol symbol;
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
@ -328,7 +341,8 @@ SymbolGenerator::ReturnType SymbolGenerator::Visit(Identifier &ident) {
}
symbol = GetOrCreateSymbol(ident.name_, ident.user_declared_, type);
} 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_) {
if (scope.in_edge_range && scope.visiting_edge && scope.visiting_edge->identifier_ &&
scope.visiting_edge->identifier_->name_ == ident.name_) {
// Prevent variable path bounds to reference the identifier which is bound
// by the variable path itself.
throw UnboundVariableError(ident.name_);
@ -430,6 +444,46 @@ bool SymbolGenerator::PreVisit(Extract &extract) {
return false;
}
bool SymbolGenerator::PreVisit(Exists &exists) {
auto &scope = scopes_.back();
if (scope.in_set_property) {
throw utils::NotYetImplemented("Set property can not be used with exists, but only during matching!");
}
if (scope.in_with) {
throw utils::NotYetImplemented("WITH can not be used with exists, but only during matching!");
}
scope.in_exists = true;
const auto &symbol = CreateAnonymousSymbol();
exists.MapTo(symbol);
return true;
}
bool SymbolGenerator::PostVisit(Exists & /*exists*/) {
auto &scope = scopes_.back();
scope.in_exists = false;
return true;
}
bool SymbolGenerator::PreVisit(SetProperty & /*set_property*/) {
auto &scope = scopes_.back();
scope.in_set_property = true;
return true;
}
bool SymbolGenerator::PostVisit(SetProperty & /*set_property*/) {
auto &scope = scopes_.back();
scope.in_set_property = false;
return true;
}
// Pattern and its subparts.
bool SymbolGenerator::PreVisit(Pattern &pattern) {
@ -439,6 +493,7 @@ bool SymbolGenerator::PreVisit(Pattern &pattern) {
MG_ASSERT(utils::IsSubtype(*pattern.atoms_[0], NodeAtom::kType), "Expected a single NodeAtom in Pattern");
scope.in_create_node = true;
}
return true;
}

View File

@ -64,6 +64,8 @@ class SymbolGenerator : public HierarchicalTreeVisitor {
bool PostVisit(Match &) override;
bool PreVisit(Foreach &) override;
bool PostVisit(Foreach &) override;
bool PreVisit(SetProperty & /*set_property*/) override;
bool PostVisit(SetProperty & /*set_property*/) override;
// Expressions
ReturnType Visit(Identifier &) override;
@ -79,6 +81,8 @@ class SymbolGenerator : public HierarchicalTreeVisitor {
bool PreVisit(None &) override;
bool PreVisit(Reduce &) override;
bool PreVisit(Extract &) override;
bool PreVisit(Exists & /*exists*/) override;
bool PostVisit(Exists & /*exists*/) override;
// Pattern and its subparts.
bool PreVisit(Pattern &) override;
@ -113,6 +117,8 @@ class SymbolGenerator : public HierarchicalTreeVisitor {
bool in_where{false};
bool in_match{false};
bool in_foreach{false};
bool in_exists{false};
bool in_set_property{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};
@ -143,6 +149,9 @@ class SymbolGenerator : public HierarchicalTreeVisitor {
auto CreateSymbol(const std::string &name, bool user_declared, Symbol::Type type = Symbol::Type::ANY,
int token_position = -1);
// Returns a freshly generated anonymous symbol.
auto CreateAnonymousSymbol(Symbol::Type type = Symbol::Type::ANY);
auto GetOrCreateSymbol(const std::string &name, bool user_declared, Symbol::Type type = Symbol::Type::ANY);
// Returns the symbol by name. If the mapping already exists, checks if the
// types match. Otherwise, returns a new symbol.

View File

@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd.
// Copyright 2023 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@ -51,6 +51,7 @@ class SymbolTable final {
const Symbol &at(const Identifier &ident) const { return table_.at(ident.symbol_pos_); }
const Symbol &at(const NamedExpression &nexpr) const { return table_.at(nexpr.symbol_pos_); }
const Symbol &at(const Aggregation &aggr) const { return table_.at(aggr.symbol_pos_); }
const Symbol &at(const Exists &exists) const { return table_.at(exists.symbol_pos_); }
// TODO: Remove these since members are public
int32_t max_position() const { return static_cast<int32_t>(table_.size()); }

View File

@ -89,6 +89,7 @@ class ReferenceExpressionEvaluator : public ExpressionVisitor<TypedValue *> {
UNSUCCESSFUL_VISIT(None);
UNSUCCESSFUL_VISIT(ParameterLookup);
UNSUCCESSFUL_VISIT(RegexMatch);
UNSUCCESSFUL_VISIT(Exists);
private:
Frame *frame_;
@ -619,6 +620,8 @@ class ExpressionEvaluator : public ExpressionVisitor<TypedValue> {
return TypedValue(result, ctx_->memory);
}
TypedValue Visit(Exists &exists) override { return TypedValue{frame_->at(symbol_table_->at(exists)), ctx_->memory}; }
TypedValue Visit(All &all) override {
auto list_value = all.list_expression_->Accept(*this);
if (list_value.IsNull()) {

View File

@ -116,6 +116,7 @@ extern const Event CartesianOperator;
extern const Event CallProcedureOperator;
extern const Event ForeachOperator;
extern const Event EmptyResultOperator;
extern const Event EvaluatePatternFilterOperator;
} // namespace EventCounter
namespace memgraph::query::plan {
@ -2257,10 +2258,19 @@ std::vector<Symbol> ConstructNamedPath::ModifiedSymbols(const SymbolTable &table
return symbols;
}
Filter::Filter(const std::shared_ptr<LogicalOperator> &input, Expression *expression)
: input_(input ? input : std::make_shared<Once>()), expression_(expression) {}
Filter::Filter(const std::shared_ptr<LogicalOperator> &input,
const std::vector<std::shared_ptr<LogicalOperator>> &pattern_filters, Expression *expression)
: input_(input ? input : std::make_shared<Once>()), pattern_filters_(pattern_filters), expression_(expression) {}
ACCEPT_WITH_INPUT(Filter)
bool Filter::Accept(HierarchicalLogicalOperatorVisitor &visitor) {
if (visitor.PreVisit(*this)) {
input_->Accept(visitor);
for (const auto &pattern_filter : pattern_filters_) {
pattern_filter->Accept(visitor);
}
}
return visitor.PostVisit(*this);
}
UniqueCursorPtr Filter::MakeCursor(utils::MemoryResource *mem) const {
EventCounter::IncrementCounter(EventCounter::FilterOperator);
@ -2270,8 +2280,24 @@ UniqueCursorPtr Filter::MakeCursor(utils::MemoryResource *mem) const {
std::vector<Symbol> Filter::ModifiedSymbols(const SymbolTable &table) const { return input_->ModifiedSymbols(table); }
static std::vector<UniqueCursorPtr> MakeCursorVector(const std::vector<std::shared_ptr<LogicalOperator>> &ops,
utils::MemoryResource *mem) {
std::vector<UniqueCursorPtr> cursors;
cursors.reserve(ops.size());
if (!ops.empty()) {
for (const auto &op : ops) {
cursors.push_back(op->MakeCursor(mem));
}
}
return cursors;
}
Filter::FilterCursor::FilterCursor(const Filter &self, utils::MemoryResource *mem)
: self_(self), input_cursor_(self_.input_->MakeCursor(mem)) {}
: self_(self),
input_cursor_(self_.input_->MakeCursor(mem)),
pattern_filter_cursors_(MakeCursorVector(self_.pattern_filters_, mem)) {}
bool Filter::FilterCursor::Pull(Frame &frame, ExecutionContext &context) {
SCOPED_PROFILE_OP("Filter");
@ -2281,6 +2307,10 @@ bool Filter::FilterCursor::Pull(Frame &frame, ExecutionContext &context) {
ExpressionEvaluator evaluator(&frame, context.symbol_table, context.evaluation_context, context.db_accessor,
storage::View::OLD);
while (input_cursor_->Pull(frame, context)) {
for (const auto &pattern_filter_cursor : pattern_filter_cursors_) {
pattern_filter_cursor->Pull(frame, context);
}
if (EvaluateFilter(evaluator, self_.expression_)) return true;
}
return false;
@ -2290,6 +2320,39 @@ void Filter::FilterCursor::Shutdown() { input_cursor_->Shutdown(); }
void Filter::FilterCursor::Reset() { input_cursor_->Reset(); }
EvaluatePatternFilter::EvaluatePatternFilter(const std::shared_ptr<LogicalOperator> &input, Symbol output_symbol)
: input_(input), output_symbol_(output_symbol) {}
ACCEPT_WITH_INPUT(EvaluatePatternFilter);
UniqueCursorPtr EvaluatePatternFilter::MakeCursor(utils::MemoryResource *mem) const {
EventCounter::IncrementCounter(EventCounter::EvaluatePatternFilterOperator);
return MakeUniqueCursorPtr<EvaluatePatternFilterCursor>(mem, *this, mem);
}
EvaluatePatternFilter::EvaluatePatternFilterCursor::EvaluatePatternFilterCursor(const EvaluatePatternFilter &self,
utils::MemoryResource *mem)
: self_(self), input_cursor_(self_.input_->MakeCursor(mem)) {}
std::vector<Symbol> EvaluatePatternFilter::ModifiedSymbols(const SymbolTable &table) const {
return input_->ModifiedSymbols(table);
}
bool EvaluatePatternFilter::EvaluatePatternFilterCursor::Pull(Frame &frame, ExecutionContext &context) {
SCOPED_PROFILE_OP("EvaluatePatternFilter");
input_cursor_->Reset();
frame[self_.output_symbol_] = TypedValue(input_cursor_->Pull(frame, context), context.evaluation_context.memory);
return true;
}
void EvaluatePatternFilter::EvaluatePatternFilterCursor::Shutdown() { input_cursor_->Shutdown(); }
void EvaluatePatternFilter::EvaluatePatternFilterCursor::Reset() { input_cursor_->Reset(); }
Produce::Produce(const std::shared_ptr<LogicalOperator> &input, const std::vector<NamedExpression *> &named_expressions)
: input_(input ? input : std::make_shared<Once>()), named_expressions_(named_expressions) {}

View File

@ -133,6 +133,7 @@ class CallProcedure;
class LoadCsv;
class Foreach;
class EmptyResult;
class EvaluatePatternFilter;
using LogicalOperatorCompositeVisitor = utils::CompositeVisitor<
Once, CreateNode, CreateExpand, ScanAll, ScanAllByLabel,
@ -141,7 +142,7 @@ using LogicalOperatorCompositeVisitor = utils::CompositeVisitor<
Expand, ExpandVariable, ConstructNamedPath, Filter, Produce, Delete,
SetProperty, SetProperties, SetLabels, RemoveProperty, RemoveLabels,
EdgeUniquenessFilter, Accumulate, Aggregate, Skip, Limit, OrderBy, Merge,
Optional, Unwind, Distinct, Union, Cartesian, CallProcedure, LoadCsv, Foreach, EmptyResult>;
Optional, Unwind, Distinct, Union, Cartesian, CallProcedure, LoadCsv, Foreach, EmptyResult, EvaluatePatternFilter>;
using LogicalOperatorLeafVisitor = utils::LeafVisitor<Once>;
@ -1122,6 +1123,9 @@ pulled.")
((input "std::shared_ptr<LogicalOperator>" :scope :public
:slk-save #'slk-save-operator-pointer
:slk-load #'slk-load-operator-pointer)
(pattern_filters "std::vector<std::shared_ptr<LogicalOperator>>" :scope :public
:slk-save #'slk-save-ast-vector
:slk-load (slk-load-ast-vector "std::shared_ptr<LogicalOperator>"))
(expression "Expression *" :scope :public
:slk-save #'slk-save-ast-pointer
:slk-load (slk-load-ast-pointer "Expression")))
@ -1136,6 +1140,7 @@ a boolean value.")
Filter() {}
Filter(const std::shared_ptr<LogicalOperator> &input_,
const std::vector<std::shared_ptr<LogicalOperator>> &pattern_filters_,
Expression *expression_);
bool Accept(HierarchicalLogicalOperatorVisitor &visitor) override;
UniqueCursorPtr MakeCursor(utils::MemoryResource *) const override;
@ -1159,6 +1164,7 @@ a boolean value.")
private:
const Filter &self_;
const UniqueCursorPtr input_cursor_;
const std::vector<UniqueCursorPtr> pattern_filter_cursors_;
};
cpp<#)
(:serialize (:slk))
@ -1777,6 +1783,45 @@ operator's implementation does not expect this.")
(:serialize (:slk))
(:clone))
(lcp:define-class evaluate-pattern-filter (logical-operator)
((input "std::shared_ptr<LogicalOperator>" :scope :public
:slk-save #'slk-save-operator-pointer
:slk-load #'slk-load-operator-pointer)
(output-symbol "Symbol" :scope :public))
(:documentation "Applies the pattern filter by putting the value of the input cursor to the frame.")
(:public
#>cpp
EvaluatePatternFilter() {}
EvaluatePatternFilter(const std::shared_ptr<LogicalOperator> &input, Symbol output_symbol);
bool Accept(HierarchicalLogicalOperatorVisitor &visitor) override;
UniqueCursorPtr MakeCursor(utils::MemoryResource *) const override;
std::vector<Symbol> ModifiedSymbols(const SymbolTable &) const override;
bool HasSingleInput() const override { return true; }
std::shared_ptr<LogicalOperator> input() const override { return input_; }
void set_input(std::shared_ptr<LogicalOperator> input) override {
input_ = input;
}
cpp<#)
(:private
#>cpp
class EvaluatePatternFilterCursor : public Cursor {
public:
EvaluatePatternFilterCursor(const EvaluatePatternFilter &, utils::MemoryResource *);
bool Pull(Frame &, ExecutionContext &) override;
void Shutdown() override;
void Reset() override;
private:
const EvaluatePatternFilter &self_;
UniqueCursorPtr input_cursor_;
};
cpp<#)
(:serialize (:slk))
(:clone))
(lcp:define-class limit (logical-operator)
((input "std::shared_ptr<LogicalOperator>" :scope :public
:slk-save #'slk-save-operator-pointer

View File

@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd.
// Copyright 2023 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@ -84,56 +84,6 @@ std::vector<Expansion> NormalizePatterns(const SymbolTable &symbol_table, const
return expansions;
}
// Fills the given Matching, by converting the Match patterns to normalized
// representation as Expansions. Filters used in the Match are also collected,
// as well as edge symbols which determine Cyphermorphism. Collecting filters
// will lift them out of a pattern and generate new expressions (just like they
// were in a Where clause).
void AddMatching(const std::vector<Pattern *> &patterns, Where *where, SymbolTable &symbol_table, AstStorage &storage,
Matching &matching) {
auto expansions = NormalizePatterns(symbol_table, patterns);
std::unordered_set<Symbol> edge_symbols;
for (const auto &expansion : expansions) {
// Matching may already have some expansions, so offset our index.
const size_t expansion_ix = matching.expansions.size();
// Map node1 symbol to expansion
const auto &node1_sym = symbol_table.at(*expansion.node1->identifier_);
matching.node_symbol_to_expansions[node1_sym].insert(expansion_ix);
// Add node1 to all symbols.
matching.expansion_symbols.insert(node1_sym);
if (expansion.edge) {
const auto &edge_sym = symbol_table.at(*expansion.edge->identifier_);
// Fill edge symbols for Cyphermorphism.
edge_symbols.insert(edge_sym);
// Map node2 symbol to expansion
const auto &node2_sym = symbol_table.at(*expansion.node2->identifier_);
matching.node_symbol_to_expansions[node2_sym].insert(expansion_ix);
// Add edge and node2 to all symbols
matching.expansion_symbols.insert(edge_sym);
matching.expansion_symbols.insert(node2_sym);
}
matching.expansions.push_back(expansion);
}
if (!edge_symbols.empty()) {
matching.edge_symbols.emplace_back(edge_symbols);
}
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);
}
}
void AddMatching(const Match &match, SymbolTable &symbol_table, AstStorage &storage, Matching &matching) {
return AddMatching(match.patterns_, match.where_, symbol_table, storage, matching);
}
auto SplitExpressionOnAnd(Expression *expression) {
// TODO: Think about converting all filtering expression into CNF to improve
// the granularity of filters which can be stand alone.
@ -519,6 +469,8 @@ void Filters::AnalyzeAndStoreFilter(Expression *expr, const SymbolTable &symbol_
if (!add_prop_is_not_null_check(is_not_null)) {
all_filters_.emplace_back(make_filter(FilterInfo::Type::Generic));
}
} else if (auto *exists = utils::Downcast<Exists>(expr)) {
all_filters_.emplace_back(make_filter(FilterInfo::Type::Pattern));
} else {
all_filters_.emplace_back(make_filter(FilterInfo::Type::Generic));
}
@ -528,6 +480,78 @@ void Filters::AnalyzeAndStoreFilter(Expression *expr, const SymbolTable &symbol_
// as `expr1 < n.prop AND n.prop < expr2`.
}
// Fills the given Matching, by converting the Match patterns to normalized
// representation as Expansions. Filters used in the Match are also collected,
// as well as edge symbols which determine Cyphermorphism. Collecting filters
// will lift them out of a pattern and generate new expressions (just like they
// were in a Where clause).
void AddMatching(const std::vector<Pattern *> &patterns, Where *where, SymbolTable &symbol_table, AstStorage &storage,
Matching &matching) {
auto expansions = NormalizePatterns(symbol_table, patterns);
std::unordered_set<Symbol> edge_symbols;
for (const auto &expansion : expansions) {
// Matching may already have some expansions, so offset our index.
const size_t expansion_ix = matching.expansions.size();
// Map node1 symbol to expansion
const auto &node1_sym = symbol_table.at(*expansion.node1->identifier_);
matching.node_symbol_to_expansions[node1_sym].insert(expansion_ix);
// Add node1 to all symbols.
matching.expansion_symbols.insert(node1_sym);
if (expansion.edge) {
const auto &edge_sym = symbol_table.at(*expansion.edge->identifier_);
// Fill edge symbols for Cyphermorphism.
edge_symbols.insert(edge_sym);
// Map node2 symbol to expansion
const auto &node2_sym = symbol_table.at(*expansion.node2->identifier_);
matching.node_symbol_to_expansions[node2_sym].insert(expansion_ix);
// Add edge and node2 to all symbols
matching.expansion_symbols.insert(edge_sym);
matching.expansion_symbols.insert(node2_sym);
}
matching.expansions.push_back(expansion);
}
if (!edge_symbols.empty()) {
matching.edge_symbols.emplace_back(edge_symbols);
}
for (auto *const pattern : patterns) {
matching.filters.CollectPatternFilters(*pattern, symbol_table, storage);
if (pattern->identifier_->user_declared_) {
std::vector<Symbol> path_elements;
for (auto *const pattern_atom : pattern->atoms_)
path_elements.push_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);
}
}
void AddMatching(const Match &match, SymbolTable &symbol_table, AstStorage &storage, Matching &matching) {
AddMatching(match.patterns_, match.where_, symbol_table, storage, matching);
// If there are any pattern filters, we add those as well
for (auto &filter : matching.filters) {
PatternFilterVisitor visitor(symbol_table, storage);
filter.expression->Accept(visitor);
filter.matchings = visitor.getMatchings();
}
}
void PatternFilterVisitor::Visit(Exists &op) {
std::vector<Pattern *> patterns;
patterns.push_back(op.pattern_);
FilterMatching filter_matching;
AddMatching(patterns, nullptr, symbol_table_, storage_, filter_matching);
filter_matching.type = PatternFilterType::EXISTS;
filter_matching.symbol = std::make_optional<Symbol>(symbol_table_.at(op));
matchings_.push_back(std::move(filter_matching));
}
static void ParseForeach(query::Foreach &foreach, SingleQueryPart &query_part, AstStorage &storage,
SymbolTable &symbol_table) {
for (auto *clause : foreach.clauses_) {

View File

@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd.
// Copyright 2023 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@ -70,7 +70,26 @@ class UsedSymbolsCollector : public HierarchicalTreeVisitor {
}
bool Visit(Identifier &ident) override {
symbols_.insert(symbol_table_.at(ident));
if (!in_exists || ident.user_declared_) {
symbols_.insert(symbol_table_.at(ident));
}
return true;
}
bool PreVisit(Exists &exists) override {
in_exists = true;
// We do not visit pattern identifier since we're in exists filter pattern
for (auto &atom : exists.pattern_->atoms_) {
atom->Accept(*this);
}
return false;
}
bool PostVisit(Exists & /*exists*/) override {
in_exists = false;
return true;
}
@ -79,6 +98,9 @@ class UsedSymbolsCollector : public HierarchicalTreeVisitor {
std::unordered_set<Symbol> symbols_;
const SymbolTable &symbol_table_;
private:
bool in_exists{false};
};
/// Normalized representation of a pattern that needs to be matched.
@ -99,6 +121,93 @@ struct Expansion {
NodeAtom *node2 = nullptr;
};
struct FilterMatching;
enum class PatternFilterType { EXISTS };
/// Collects matchings from filters that include patterns
class PatternFilterVisitor : public ExpressionVisitor<void> {
public:
explicit PatternFilterVisitor(SymbolTable &symbol_table, AstStorage &storage)
: symbol_table_(symbol_table), storage_(storage) {}
using ExpressionVisitor<void>::Visit;
// Unary operators
void Visit(NotOperator &op) override { op.expression_->Accept(*this); }
void Visit(IsNullOperator &op) override { op.expression_->Accept(*this); };
void Visit(UnaryPlusOperator &op) override{};
void Visit(UnaryMinusOperator &op) override{};
// Binary operators
void Visit(OrOperator &op) override {
op.expression1_->Accept(*this);
op.expression2_->Accept(*this);
}
void Visit(XorOperator &op) override {
op.expression1_->Accept(*this);
op.expression2_->Accept(*this);
}
void Visit(AndOperator &op) override {
op.expression1_->Accept(*this);
op.expression2_->Accept(*this);
}
void Visit(NotEqualOperator &op) override {
op.expression1_->Accept(*this);
op.expression2_->Accept(*this);
};
void Visit(EqualOperator &op) override {
op.expression1_->Accept(*this);
op.expression2_->Accept(*this);
};
void Visit(InListOperator &op) override {
op.expression1_->Accept(*this);
op.expression2_->Accept(*this);
};
void Visit(AdditionOperator &op) override{};
void Visit(SubtractionOperator &op) override{};
void Visit(MultiplicationOperator &op) override{};
void Visit(DivisionOperator &op) override{};
void Visit(ModOperator &op) override{};
void Visit(LessOperator &op) override{};
void Visit(GreaterOperator &op) override{};
void Visit(LessEqualOperator &op) override{};
void Visit(GreaterEqualOperator &op) override{};
void Visit(SubscriptOperator &op) override{};
// Other
void Visit(ListSlicingOperator &op) override{};
void Visit(IfOperator &op) override{};
void Visit(ListLiteral &op) override{};
void Visit(MapLiteral &op) override{};
void Visit(LabelsTest &op) override{};
void Visit(Aggregation &op) override{};
void Visit(Function &op) override{};
void Visit(Reduce &op) override{};
void Visit(Coalesce &op) override{};
void Visit(Extract &op) override{};
void Visit(Exists &op) override;
void Visit(All &op) override{};
void Visit(Single &op) override{};
void Visit(Any &op) override{};
void Visit(None &op) override{};
void Visit(Identifier &op) override{};
void Visit(PrimitiveLiteral &op) override{};
void Visit(PropertyLookup &op) override{};
void Visit(ParameterLookup &op) override{};
void Visit(NamedExpression &op) override{};
void Visit(RegexMatch &op) override{};
std::vector<FilterMatching> getMatchings() { return matchings_; }
SymbolTable &symbol_table_;
AstStorage &storage_;
private:
/// Collection of matchings in the filter expression being analyzed.
std::vector<FilterMatching> matchings_;
};
/// Stores the symbols and expression used to filter a property.
class PropertyFilter {
public:
@ -153,7 +262,7 @@ struct FilterInfo {
/// applied for labels or a property. Non generic types contain extra
/// information which can be used to produce indexed scans of graph
/// elements.
enum class Type { Generic, Label, Property, Id };
enum class Type { Generic, Label, Property, Id, Pattern };
Type type;
/// The original filter expression which must be satisfied.
@ -166,6 +275,8 @@ struct FilterInfo {
std::optional<PropertyFilter> property_filter;
/// Information for Type::Id filtering.
std::optional<IdFilter> id_filter;
/// Matchings for filters that include patterns
std::vector<FilterMatching> matchings;
};
/// Stores information on filters used inside the @c Matching of a @c QueryPart.
@ -287,6 +398,13 @@ struct Matching {
std::unordered_set<Symbol> expansion_symbols{};
};
struct FilterMatching : Matching {
/// Type of pattern filter
PatternFilterType type;
/// Symbol for the filter expression
std::optional<Symbol> symbol;
};
/// @brief Represents a read (+ write) part of a query. Parts are split on
/// `WITH` clauses.
///

View File

@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd.
// Copyright 2023 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@ -14,6 +14,7 @@
#include "query/db_accessor.hpp"
#include "query/frontend/ast/pretty_print.hpp"
#include "query/plan/operator.hpp"
#include "utils/string.hpp"
namespace memgraph::query::plan {
@ -148,7 +149,6 @@ bool PlanPrinter::PreVisit(query::plan::Produce &op) {
}
PRE_VISIT(ConstructNamedPath);
PRE_VISIT(Filter);
PRE_VISIT(SetProperty);
PRE_VISIT(SetProperties);
PRE_VISIT(SetLabels);
@ -157,6 +157,7 @@ PRE_VISIT(RemoveLabels);
PRE_VISIT(EdgeUniquenessFilter);
PRE_VISIT(Accumulate);
PRE_VISIT(EmptyResult);
PRE_VISIT(EvaluatePatternFilter);
bool PlanPrinter::PreVisit(query::plan::Aggregate &op) {
WithPrintLn([&](auto &out) {
@ -251,6 +252,15 @@ bool PlanPrinter::PreVisit(query::plan::Foreach &op) {
op.input_->Accept(*this);
return false;
}
bool PlanPrinter::PreVisit(query::plan::Filter &op) {
WithPrintLn([](auto &out) { out << "* Filter"; });
for (const auto &pattern_filter : op.pattern_filters_) {
Branch(*pattern_filter);
}
op.input_->Accept(*this);
return false;
}
#undef PRE_VISIT
bool PlanPrinter::DefaultPreVisit() {
@ -589,6 +599,13 @@ bool PlanToJsonVisitor::PreVisit(Filter &op) {
op.input_->Accept(*this);
self["input"] = PopOutput();
for (auto pattern_idx = 0; pattern_idx < op.pattern_filters_.size(); pattern_idx++) {
auto pattern_filter_key = "pattern_filter" + std::to_string(pattern_idx + 1);
op.pattern_filters_[pattern_idx]->Accept(*this);
self[pattern_filter_key] = PopOutput();
}
output_ = std::move(self);
return false;
}
@ -908,6 +925,7 @@ bool PlanToJsonVisitor::PreVisit(Cartesian &op) {
output_ = std::move(self);
return false;
}
bool PlanToJsonVisitor::PreVisit(Foreach &op) {
json self;
self["name"] = "Foreach";
@ -924,6 +942,18 @@ bool PlanToJsonVisitor::PreVisit(Foreach &op) {
return false;
}
bool PlanToJsonVisitor::PreVisit(EvaluatePatternFilter &op) {
json self;
self["name"] = "EvaluatePatternFilter";
self["output_symbol"] = ToJson(op.output_symbol_);
op.input_->Accept(*this);
self["input"] = PopOutput();
output_ = std::move(self);
return false;
}
} // namespace impl
} // namespace memgraph::query::plan

View File

@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd.
// Copyright 2023 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@ -74,6 +74,7 @@ class PlanPrinter : public virtual HierarchicalLogicalOperatorVisitor {
bool PreVisit(ConstructNamedPath &) override;
bool PreVisit(Filter &) override;
bool PreVisit(EvaluatePatternFilter & /*unused*/) override;
bool PreVisit(EdgeUniquenessFilter &) override;
bool PreVisit(Merge &) override;
@ -186,6 +187,7 @@ class PlanToJsonVisitor : public virtual HierarchicalLogicalOperatorVisitor {
bool PreVisit(Optional &) override;
bool PreVisit(Filter &) override;
bool PreVisit(EvaluatePatternFilter & /*op*/) override;
bool PreVisit(EdgeUniquenessFilter &) override;
bool PreVisit(Cartesian &) override;

View File

@ -454,6 +454,16 @@ class IndexLookupRewriter final : public HierarchicalLogicalOperatorVisitor {
return true;
}
bool PreVisit(EvaluatePatternFilter &op) override {
prev_ops_.push_back(&op);
return true;
}
bool PostVisit(EvaluatePatternFilter & /*op*/) override {
prev_ops_.pop_back();
return true;
}
std::shared_ptr<LogicalOperator> new_root_;
private:

View File

@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd.
// Copyright 2023 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@ -25,15 +25,6 @@ namespace memgraph::query::plan {
namespace {
bool HasBoundFilterSymbols(const std::unordered_set<Symbol> &bound_symbols, const FilterInfo &filter) {
for (const auto &symbol : filter.used_symbols) {
if (bound_symbols.find(symbol) == bound_symbols.end()) {
return false;
}
}
return true;
}
// Ast tree visitor which collects the context for a return body.
// The return body of WITH and RETURN clauses consists of:
//
@ -495,7 +486,8 @@ std::unique_ptr<LogicalOperator> GenReturnBody(std::unique_ptr<LogicalOperator>
// Where may see new symbols so it comes after we generate Produce and in
// general, comes after any OrderBy, Skip or Limit.
if (body.where()) {
last_op = std::make_unique<Filter>(std::move(last_op), body.where()->expression_);
last_op = std::make_unique<Filter>(std::move(last_op), std::vector<std::shared_ptr<LogicalOperator>>{},
body.where()->expression_);
}
return last_op;
}
@ -504,6 +496,12 @@ std::unique_ptr<LogicalOperator> GenReturnBody(std::unique_ptr<LogicalOperator>
namespace impl {
bool HasBoundFilterSymbols(const std::unordered_set<Symbol> &bound_symbols, const FilterInfo &filter) {
return std::ranges::all_of(
filter.used_symbols.begin(), filter.used_symbols.end(),
[&bound_symbols](const auto &symbol) { return bound_symbols.find(symbol) != bound_symbols.end(); });
}
Expression *ExtractFilters(const std::unordered_set<Symbol> &bound_symbols, Filters &filters, AstStorage &storage) {
Expression *filter_expr = nullptr;
for (auto filters_it = filters.begin(); filters_it != filters.end();) {
@ -517,16 +515,6 @@ Expression *ExtractFilters(const std::unordered_set<Symbol> &bound_symbols, Filt
return filter_expr;
}
std::unique_ptr<LogicalOperator> GenFilters(std::unique_ptr<LogicalOperator> last_op,
const std::unordered_set<Symbol> &bound_symbols, Filters &filters,
AstStorage &storage) {
auto *filter_expr = ExtractFilters(bound_symbols, filters, storage);
if (filter_expr) {
last_op = std::make_unique<Filter>(std::move(last_op), filter_expr);
}
return last_op;
}
std::unique_ptr<LogicalOperator> GenNamedPaths(std::unique_ptr<LogicalOperator> last_op,
std::unordered_set<Symbol> &bound_symbols,
std::unordered_map<Symbol, std::vector<Symbol>> &named_paths) {

View File

@ -81,8 +81,8 @@ namespace impl {
// removed from `Filters`.
Expression *ExtractFilters(const std::unordered_set<Symbol> &, Filters &, AstStorage &);
std::unique_ptr<LogicalOperator> GenFilters(std::unique_ptr<LogicalOperator>, const std::unordered_set<Symbol> &,
Filters &, AstStorage &);
/// Checks if the filters has all the bound symbols to be included in the current part of the query
bool HasBoundFilterSymbols(const std::unordered_set<Symbol> &bound_symbols, const FilterInfo &filter);
/// Utility function for iterating pattern atoms and accumulating a result.
///
@ -398,119 +398,11 @@ class RuleBasedPlanner {
// 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.
auto last_op = impl::GenFilters(std::move(input_op), bound_symbols, filters, storage);
for (const auto &expansion : matching.expansions) {
const auto &node1_symbol = symbol_table.at(*expansion.node1->identifier_);
if (bound_symbols.insert(node1_symbol).second) {
// We have just bound this symbol, so generate ScanAll which fills it.
last_op = std::make_unique<ScanAll>(std::move(last_op), node1_symbol, match_context.view);
match_context.new_symbols.emplace_back(node1_symbol);
last_op = impl::GenFilters(std::move(last_op), bound_symbols, filters, storage);
last_op = impl::GenNamedPaths(std::move(last_op), bound_symbols, named_paths);
last_op = impl::GenFilters(std::move(last_op), bound_symbols, filters, storage);
}
// We have an edge, so generate Expand.
if (expansion.edge) {
auto *edge = expansion.edge;
// If the expand symbols were already bound, then we need to indicate
// that they exist. The Expand will then check whether the pattern holds
// instead of writing the expansion to symbols.
const auto &node_symbol = symbol_table.at(*expansion.node2->identifier_);
auto existing_node = utils::Contains(bound_symbols, node_symbol);
const auto &edge_symbol = symbol_table.at(*edge->identifier_);
MG_ASSERT(!utils::Contains(bound_symbols, edge_symbol), "Existing edges are not supported");
std::vector<storage::EdgeTypeId> edge_types;
edge_types.reserve(edge->edge_types_.size());
for (const auto &type : edge->edge_types_) {
edge_types.push_back(GetEdgeType(type));
}
if (edge->IsVariable()) {
std::optional<ExpansionLambda> weight_lambda;
std::optional<Symbol> total_weight;
auto last_op = GenFilters(std::move(input_op), bound_symbols, filters, storage, symbol_table);
if (edge->type_ == EdgeAtom::Type::WEIGHTED_SHORTEST_PATH ||
edge->type_ == EdgeAtom::Type::ALL_SHORTEST_PATHS) {
weight_lambda.emplace(ExpansionLambda{symbol_table.at(*edge->weight_lambda_.inner_edge),
symbol_table.at(*edge->weight_lambda_.inner_node),
edge->weight_lambda_.expression});
last_op = HandleExpansion(std::move(last_op), matching, symbol_table, storage, bound_symbols,
match_context.new_symbols, named_paths, filters, match_context.view);
total_weight.emplace(symbol_table.at(*edge->total_weight_));
}
ExpansionLambda filter_lambda;
filter_lambda.inner_edge_symbol = symbol_table.at(*edge->filter_lambda_.inner_edge);
filter_lambda.inner_node_symbol = symbol_table.at(*edge->filter_lambda_.inner_node);
{
// Bind the inner edge and node symbols so they're available for
// inline filtering in ExpandVariable.
bool inner_edge_bound = bound_symbols.insert(filter_lambda.inner_edge_symbol).second;
bool inner_node_bound = bound_symbols.insert(filter_lambda.inner_node_symbol).second;
MG_ASSERT(inner_edge_bound && inner_node_bound, "An inner edge and node can't be bound from before");
}
// Join regular filters with lambda filter expression, so that they
// are done inline together. Semantic analysis should guarantee that
// lambda filtering uses bound symbols.
filter_lambda.expression = impl::BoolJoin<AndOperator>(
storage, impl::ExtractFilters(bound_symbols, filters, storage), edge->filter_lambda_.expression);
// At this point it's possible we have leftover filters for inline
// filtering (they use the inner symbols. If they were not collected,
// we have to remove them manually because no other filter-extraction
// will ever bind them again.
filters.erase(std::remove_if(
filters.begin(), filters.end(),
[e = filter_lambda.inner_edge_symbol, n = filter_lambda.inner_node_symbol](FilterInfo &fi) {
return utils::Contains(fi.used_symbols, e) || utils::Contains(fi.used_symbols, n);
}),
filters.end());
// Unbind the temporarily bound inner symbols for filtering.
bound_symbols.erase(filter_lambda.inner_edge_symbol);
bound_symbols.erase(filter_lambda.inner_node_symbol);
if (total_weight) {
bound_symbols.insert(*total_weight);
}
// TODO: Pass weight lambda.
MG_ASSERT(match_context.view == storage::View::OLD,
"ExpandVariable should only be planned with storage::View::OLD");
last_op = std::make_unique<ExpandVariable>(std::move(last_op), node1_symbol, node_symbol, edge_symbol,
edge->type_, expansion.direction, edge_types, expansion.is_flipped,
edge->lower_bound_, edge->upper_bound_, existing_node,
filter_lambda, weight_lambda, total_weight);
} else {
last_op = std::make_unique<Expand>(std::move(last_op), node1_symbol, node_symbol, edge_symbol,
expansion.direction, edge_types, existing_node, match_context.view);
}
// Bind the expanded edge and node.
bound_symbols.insert(edge_symbol);
match_context.new_symbols.emplace_back(edge_symbol);
if (bound_symbols.insert(node_symbol).second) {
match_context.new_symbols.emplace_back(node_symbol);
}
// Ensure Cyphermorphism (different edge symbols always map to
// different edges).
for (const auto &edge_symbols : matching.edge_symbols) {
if (edge_symbols.find(edge_symbol) == edge_symbols.end()) {
continue;
}
std::vector<Symbol> other_symbols;
for (const auto &symbol : edge_symbols) {
if (symbol == edge_symbol || bound_symbols.find(symbol) == bound_symbols.end()) {
continue;
}
other_symbols.push_back(symbol);
}
if (!other_symbols.empty()) {
last_op = std::make_unique<EdgeUniquenessFilter>(std::move(last_op), edge_symbol, other_symbols);
}
}
last_op = impl::GenFilters(std::move(last_op), bound_symbols, filters, storage);
last_op = impl::GenNamedPaths(std::move(last_op), bound_symbols, named_paths);
last_op = impl::GenFilters(std::move(last_op), bound_symbols, filters, storage);
}
}
MG_ASSERT(named_paths.empty(), "Expected to generate all named paths");
// We bound all named path symbols, so just add them to new_symbols.
for (const auto &named_path : matching.named_paths) {
@ -547,6 +439,143 @@ class RuleBasedPlanner {
return std::make_unique<plan::Merge>(std::move(input_op), std::move(on_match), std::move(on_create));
}
std::unique_ptr<LogicalOperator> HandleExpansion(std::unique_ptr<LogicalOperator> last_op, const Matching &matching,
const SymbolTable &symbol_table, AstStorage &storage,
std::unordered_set<Symbol> &bound_symbols,
std::vector<Symbol> &new_symbols,
std::unordered_map<Symbol, std::vector<Symbol>> &named_paths,
Filters &filters, storage::View view) {
for (const auto &expansion : matching.expansions) {
const auto &node1_symbol = symbol_table.at(*expansion.node1->identifier_);
if (bound_symbols.insert(node1_symbol).second) {
// We have just bound this symbol, so generate ScanAll which fills it.
last_op = std::make_unique<ScanAll>(std::move(last_op), node1_symbol, view);
new_symbols.emplace_back(node1_symbol);
last_op = GenFilters(std::move(last_op), bound_symbols, filters, storage, symbol_table);
last_op = impl::GenNamedPaths(std::move(last_op), bound_symbols, named_paths);
last_op = GenFilters(std::move(last_op), bound_symbols, filters, storage, symbol_table);
}
if (expansion.edge) {
last_op = GenExpand(std::move(last_op), expansion, symbol_table, bound_symbols, matching, storage, filters,
named_paths, new_symbols, view);
}
}
return last_op;
}
std::unique_ptr<LogicalOperator> GenExpand(std::unique_ptr<LogicalOperator> last_op, const Expansion &expansion,
const SymbolTable &symbol_table, std::unordered_set<Symbol> &bound_symbols,
const Matching &matching, AstStorage &storage, Filters &filters,
std::unordered_map<Symbol, std::vector<Symbol>> &named_paths,
std::vector<Symbol> &new_symbols, storage::View view) {
// If the expand symbols were already bound, then we need to indicate
// that they exist. The Expand will then check whether the pattern holds
// instead of writing the expansion to symbols.
const auto &node1_symbol = symbol_table.at(*expansion.node1->identifier_);
bound_symbols.insert(node1_symbol);
const auto &node_symbol = symbol_table.at(*expansion.node2->identifier_);
auto *edge = expansion.edge;
auto existing_node = utils::Contains(bound_symbols, node_symbol);
const auto &edge_symbol = symbol_table.at(*edge->identifier_);
MG_ASSERT(!utils::Contains(bound_symbols, edge_symbol), "Existing edges are not supported");
std::vector<storage::EdgeTypeId> edge_types;
edge_types.reserve(edge->edge_types_.size());
for (const auto &type : edge->edge_types_) {
edge_types.push_back(GetEdgeType(type));
}
if (edge->IsVariable()) {
std::optional<ExpansionLambda> weight_lambda;
std::optional<Symbol> total_weight;
if (edge->type_ == EdgeAtom::Type::WEIGHTED_SHORTEST_PATH || edge->type_ == EdgeAtom::Type::ALL_SHORTEST_PATHS) {
weight_lambda.emplace(ExpansionLambda{symbol_table.at(*edge->weight_lambda_.inner_edge),
symbol_table.at(*edge->weight_lambda_.inner_node),
edge->weight_lambda_.expression});
total_weight.emplace(symbol_table.at(*edge->total_weight_));
}
ExpansionLambda filter_lambda;
filter_lambda.inner_edge_symbol = symbol_table.at(*edge->filter_lambda_.inner_edge);
filter_lambda.inner_node_symbol = symbol_table.at(*edge->filter_lambda_.inner_node);
{
// Bind the inner edge and node symbols so they're available for
// inline filtering in ExpandVariable.
bool inner_edge_bound = bound_symbols.insert(filter_lambda.inner_edge_symbol).second;
bool inner_node_bound = bound_symbols.insert(filter_lambda.inner_node_symbol).second;
MG_ASSERT(inner_edge_bound && inner_node_bound, "An inner edge and node can't be bound from before");
}
// Join regular filters with lambda filter expression, so that they
// are done inline together. Semantic analysis should guarantee that
// lambda filtering uses bound symbols.
filter_lambda.expression = impl::BoolJoin<AndOperator>(
storage, impl::ExtractFilters(bound_symbols, filters, storage), edge->filter_lambda_.expression);
// At this point it's possible we have leftover filters for inline
// filtering (they use the inner symbols. If they were not collected,
// we have to remove them manually because no other filter-extraction
// will ever bind them again.
filters.erase(
std::remove_if(filters.begin(), filters.end(),
[e = filter_lambda.inner_edge_symbol, n = filter_lambda.inner_node_symbol](FilterInfo &fi) {
return utils::Contains(fi.used_symbols, e) || utils::Contains(fi.used_symbols, n);
}),
filters.end());
// Unbind the temporarily bound inner symbols for filtering.
bound_symbols.erase(filter_lambda.inner_edge_symbol);
bound_symbols.erase(filter_lambda.inner_node_symbol);
if (total_weight) {
bound_symbols.insert(*total_weight);
}
// TODO: Pass weight lambda.
MG_ASSERT(view == storage::View::OLD, "ExpandVariable should only be planned with storage::View::OLD");
last_op = std::make_unique<ExpandVariable>(std::move(last_op), node1_symbol, node_symbol, edge_symbol,
edge->type_, expansion.direction, edge_types, expansion.is_flipped,
edge->lower_bound_, edge->upper_bound_, existing_node, filter_lambda,
weight_lambda, total_weight);
} else {
last_op = std::make_unique<Expand>(std::move(last_op), node1_symbol, node_symbol, edge_symbol,
expansion.direction, edge_types, existing_node, view);
}
// Bind the expanded edge and node.
bound_symbols.insert(edge_symbol);
new_symbols.emplace_back(edge_symbol);
if (bound_symbols.insert(node_symbol).second) {
new_symbols.emplace_back(node_symbol);
}
// Ensure Cyphermorphism (different edge symbols always map to
// different edges).
for (const auto &edge_symbols : matching.edge_symbols) {
if (edge_symbols.find(edge_symbol) == edge_symbols.end()) {
continue;
}
std::vector<Symbol> other_symbols;
for (const auto &symbol : edge_symbols) {
if (symbol == edge_symbol || bound_symbols.find(symbol) == bound_symbols.end()) {
continue;
}
other_symbols.push_back(symbol);
}
if (!other_symbols.empty()) {
last_op = std::make_unique<EdgeUniquenessFilter>(std::move(last_op), edge_symbol, other_symbols);
}
}
last_op = GenFilters(std::move(last_op), bound_symbols, filters, storage, symbol_table);
last_op = impl::GenNamedPaths(std::move(last_op), bound_symbols, named_paths);
last_op = GenFilters(std::move(last_op), bound_symbols, filters, storage, symbol_table);
return last_op;
}
std::unique_ptr<LogicalOperator> HandleForeachClause(query::Foreach *foreach,
std::unique_ptr<LogicalOperator> input_op,
const SymbolTable &symbol_table,
@ -567,6 +596,64 @@ class RuleBasedPlanner {
return std::make_unique<plan::Foreach>(std::move(input_op), std::move(op), foreach->named_expression_->expression_,
symbol);
}
std::unique_ptr<LogicalOperator> GenFilters(std::unique_ptr<LogicalOperator> last_op,
const std::unordered_set<Symbol> &bound_symbols, Filters &filters,
AstStorage &storage, const SymbolTable &symbol_table) {
auto pattern_filters = ExtractPatternFilters(filters, symbol_table, storage, bound_symbols);
auto *filter_expr = impl::ExtractFilters(bound_symbols, filters, storage);
if (filter_expr) {
last_op = std::make_unique<Filter>(std::move(last_op), std::move(pattern_filters), filter_expr);
}
return last_op;
}
std::unique_ptr<LogicalOperator> MakeExistsFilter(const FilterMatching &matching, const SymbolTable &symbol_table,
AstStorage &storage,
const std::unordered_set<Symbol> &bound_symbols) {
std::vector<Symbol> once_symbols(bound_symbols.begin(), bound_symbols.end());
std::unique_ptr<LogicalOperator> last_op = std::make_unique<Once>(once_symbols);
std::vector<Symbol> new_symbols;
std::unordered_set<Symbol> expand_symbols(bound_symbols.begin(), bound_symbols.end());
auto filters = matching.filters;
std::unordered_map<Symbol, std::vector<Symbol>> named_paths;
last_op = HandleExpansion(std::move(last_op), matching, symbol_table, storage, expand_symbols, new_symbols,
named_paths, filters, storage::View::OLD);
last_op = std::make_unique<Limit>(std::move(last_op), storage.Create<PrimitiveLiteral>(1));
last_op = std::make_unique<EvaluatePatternFilter>(std::move(last_op), matching.symbol.value());
return last_op;
}
std::vector<std::shared_ptr<LogicalOperator>> ExtractPatternFilters(Filters &filters, const SymbolTable &symbol_table,
AstStorage &storage,
const std::unordered_set<Symbol> &bound_symbols) {
std::vector<std::shared_ptr<LogicalOperator>> operators;
for (const auto &filter : filters) {
for (const auto &matching : filter.matchings) {
if (!impl::HasBoundFilterSymbols(bound_symbols, filter)) {
continue;
}
switch (matching.type) {
case PatternFilterType::EXISTS: {
operators.push_back(MakeExistsFilter(matching, symbol_table, storage, bound_symbols));
break;
}
}
}
}
return operators;
}
};
} // namespace memgraph::query::plan

View File

@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd.
// Copyright 2023 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@ -155,6 +155,18 @@ auto ExpansionNodes(const std::vector<Expansion> &expansions, const SymbolTable
return nodes;
}
FilterMatching ToFilterMatching(Matching &matching) {
FilterMatching filter_matching;
filter_matching.expansions = matching.expansions;
filter_matching.edge_symbols = matching.edge_symbols;
filter_matching.filters = matching.filters;
filter_matching.node_symbol_to_expansions = matching.node_symbol_to_expansions;
filter_matching.named_paths = matching.named_paths;
filter_matching.expansion_symbols = matching.expansion_symbols;
return filter_matching;
}
} // namespace
VaryMatchingStart::VaryMatchingStart(Matching matching, const SymbolTable &symbol_table)
@ -209,11 +221,31 @@ CartesianProduct<VaryMatchingStart> VaryMultiMatchingStarts(const std::vector<Ma
return MakeCartesianProduct(std::move(variants));
}
CartesianProduct<VaryMatchingStart> VaryFilterMatchingStarts(const Matching &matching,
const SymbolTable &symbol_table) {
auto filter_matchings_cnt = 0;
for (const auto &filter : matching.filters) {
filter_matchings_cnt += static_cast<int>(filter.matchings.size());
}
std::vector<VaryMatchingStart> variants;
variants.reserve(filter_matchings_cnt);
for (const auto &filter : matching.filters) {
for (const auto &filter_matching : filter.matchings) {
variants.emplace_back(filter_matching, symbol_table);
}
}
return MakeCartesianProduct(std::move(variants));
}
VaryQueryPartMatching::VaryQueryPartMatching(SingleQueryPart query_part, const SymbolTable &symbol_table)
: query_part_(std::move(query_part)),
matchings_(VaryMatchingStart(query_part_.matching, symbol_table)),
optional_matchings_(VaryMultiMatchingStarts(query_part_.optional_matching, symbol_table)),
merge_matchings_(VaryMultiMatchingStarts(query_part_.merge_matching, symbol_table)) {}
merge_matchings_(VaryMultiMatchingStarts(query_part_.merge_matching, symbol_table)),
filter_matchings_(VaryFilterMatchingStarts(query_part_.matching, symbol_table)) {}
VaryQueryPartMatching::iterator::iterator(const SingleQueryPart &query_part,
VaryMatchingStart::iterator matchings_begin,
@ -221,7 +253,9 @@ VaryQueryPartMatching::iterator::iterator(const SingleQueryPart &query_part,
CartesianProduct<VaryMatchingStart>::iterator optional_begin,
CartesianProduct<VaryMatchingStart>::iterator optional_end,
CartesianProduct<VaryMatchingStart>::iterator merge_begin,
CartesianProduct<VaryMatchingStart>::iterator merge_end)
CartesianProduct<VaryMatchingStart>::iterator merge_end,
CartesianProduct<VaryMatchingStart>::iterator filter_begin,
CartesianProduct<VaryMatchingStart>::iterator filter_end)
: current_query_part_(query_part),
matchings_it_(matchings_begin),
matchings_end_(matchings_end),
@ -230,7 +264,10 @@ VaryQueryPartMatching::iterator::iterator(const SingleQueryPart &query_part,
optional_end_(optional_end),
merge_it_(merge_begin),
merge_begin_(merge_begin),
merge_end_(merge_end) {
merge_end_(merge_end),
filter_it_(filter_begin),
filter_begin_(filter_begin),
filter_end_(filter_end) {
if (matchings_it_ != matchings_end_) {
// Fill the query part with the first variation of matchings
SetCurrentQueryPart();
@ -242,26 +279,37 @@ VaryQueryPartMatching::iterator &VaryQueryPartMatching::iterator::operator++() {
// * matchings (m1) and (m2)
// * optional matchings (o1) and (o2)
// * merge matching (g1)
// * filter matching (f1) and (f2)
// We want to produce parts for:
// * (m1), (o1), (g1)
// * (m1), (o2), (g1)
// * (m2), (o1), (g1)
// * (m2), (o2), (g1)
// Create variations by changing the merge part first.
if (merge_it_ != merge_end_) ++merge_it_;
// If all merge variations are done, start them from beginning and move to the
// next optional matching variation.
if (merge_it_ == merge_end_) {
// * (m1), (o1), (g1), (f1)
// * (m1), (o1), (g1), (f2)
// * (m1), (o2), (g1), (f1)
// * (m1), (o2), (g1), (f2)
// * (m2), (o1), (g1), (f1)
// * (m2), (o1), (g1), (f2)
// * (m2), (o2), (g1), (f1)
// * (m2), (o2), (g1), (f2)
// Create variations by changing the filter part first.
if (filter_it_ != filter_end_) ++filter_it_;
// Create variations by changing the merge part.
if (filter_it_ == filter_end_) {
filter_it_ = filter_begin_;
if (merge_it_ != merge_end_) ++merge_it_;
}
// Create variations by changing the optional part.
if (merge_it_ == merge_end_ && filter_it_ == filter_begin_) {
merge_it_ = merge_begin_;
if (optional_it_ != optional_end_) ++optional_it_;
}
// If all optional matching variations are done (after exhausting merge
// variations), start them from beginning and move to the next regular
// matching variation.
if (optional_it_ == optional_end_ && merge_it_ == merge_begin_) {
if (optional_it_ == optional_end_ && merge_it_ == merge_begin_ && filter_it_ == filter_begin_) {
optional_it_ = optional_begin_;
if (matchings_it_ != matchings_end_) ++matchings_it_;
}
// We have reached the end, so return;
if (matchings_it_ == matchings_end_) return *this;
// Fill the query part with the new variation of matchings.
@ -283,6 +331,28 @@ void VaryQueryPartMatching::iterator::SetCurrentQueryPart() {
if (merge_it_ != merge_end_) {
current_query_part_.merge_matching = *merge_it_;
}
DMG_ASSERT(filter_it_ != filter_end_ || filter_begin_ == filter_end_,
"Either there are no filter matchings or we can always generate"
"a variation");
auto all_filter_matchings = *filter_it_;
auto all_filter_matchings_idx = 0;
for (auto &filter : current_query_part_.matching.filters) {
auto matchings_size = filter.matchings.size();
std::vector<FilterMatching> new_matchings;
new_matchings.reserve(matchings_size);
for (auto i = 0; i < matchings_size; i++) {
new_matchings.push_back(ToFilterMatching(all_filter_matchings[all_filter_matchings_idx]));
new_matchings[i].symbol = filter.matchings[i].symbol;
new_matchings[i].type = filter.matchings[i].type;
all_filter_matchings_idx++;
}
filter.matchings = std::move(new_matchings);
}
}
bool VaryQueryPartMatching::iterator::operator==(const iterator &other) const {
@ -291,7 +361,8 @@ bool VaryQueryPartMatching::iterator::operator==(const iterator &other) const {
// iterators can be at any position.
return true;
}
return matchings_it_ == other.matchings_it_ && optional_it_ == other.optional_it_ && merge_it_ == other.merge_it_;
return matchings_it_ == other.matchings_it_ && optional_it_ == other.optional_it_ && merge_it_ == other.merge_it_ &&
filter_it_ == other.filter_it_;
}
} // namespace memgraph::query::plan::impl

View File

@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd.
// Copyright 2023 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@ -230,6 +230,8 @@ class VaryMatchingStart {
// Cartesian product of all of them is returned.
CartesianProduct<VaryMatchingStart> VaryMultiMatchingStarts(const std::vector<Matching> &, const SymbolTable &);
CartesianProduct<VaryMatchingStart> VaryFilterMatchingStarts(const Matching &matching, const SymbolTable &symbol_table);
// Produces alternative query parts out of a single part by varying how each
// graph matching is done.
class VaryQueryPartMatching {
@ -245,6 +247,7 @@ class VaryQueryPartMatching {
typedef const SingleQueryPart *pointer;
iterator(const SingleQueryPart &, VaryMatchingStart::iterator, VaryMatchingStart::iterator,
CartesianProduct<VaryMatchingStart>::iterator, CartesianProduct<VaryMatchingStart>::iterator,
CartesianProduct<VaryMatchingStart>::iterator, CartesianProduct<VaryMatchingStart>::iterator,
CartesianProduct<VaryMatchingStart>::iterator, CartesianProduct<VaryMatchingStart>::iterator);
@ -266,15 +269,20 @@ class VaryQueryPartMatching {
CartesianProduct<VaryMatchingStart>::iterator merge_it_;
CartesianProduct<VaryMatchingStart>::iterator merge_begin_;
CartesianProduct<VaryMatchingStart>::iterator merge_end_;
CartesianProduct<VaryMatchingStart>::iterator filter_it_;
CartesianProduct<VaryMatchingStart>::iterator filter_begin_;
CartesianProduct<VaryMatchingStart>::iterator filter_end_;
};
auto begin() {
return iterator(query_part_, matchings_.begin(), matchings_.end(), optional_matchings_.begin(),
optional_matchings_.end(), merge_matchings_.begin(), merge_matchings_.end());
optional_matchings_.end(), merge_matchings_.begin(), merge_matchings_.end(),
filter_matchings_.begin(), filter_matchings_.end());
}
auto end() {
return iterator(query_part_, matchings_.end(), matchings_.end(), optional_matchings_.end(),
optional_matchings_.end(), merge_matchings_.end(), merge_matchings_.end());
optional_matchings_.end(), merge_matchings_.end(), merge_matchings_.end(), filter_matchings_.end(),
filter_matchings_.end());
}
private:
@ -286,6 +294,7 @@ class VaryQueryPartMatching {
CartesianProduct<VaryMatchingStart> optional_matchings_;
// Like optional matching, but for merge matchings.
CartesianProduct<VaryMatchingStart> merge_matchings_;
CartesianProduct<VaryMatchingStart> filter_matchings_;
};
} // namespace impl

View File

@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd.
// Copyright 2023 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@ -146,10 +146,10 @@ struct mgp_date {
mgp_date(const memgraph::utils::Date &date, memgraph::utils::MemoryResource *memory) noexcept
: memory(memory), date(date) {}
mgp_date(const std::string_view string, memgraph::utils::MemoryResource *memory) noexcept
mgp_date(const std::string_view string, memgraph::utils::MemoryResource *memory)
: memory(memory), date(memgraph::utils::ParseDateParameters(string).first) {}
mgp_date(const mgp_date_parameters *parameters, memgraph::utils::MemoryResource *memory) noexcept
mgp_date(const mgp_date_parameters *parameters, memgraph::utils::MemoryResource *memory)
: memory(memory), date(MapDateParameters(parameters)) {}
mgp_date(const int64_t microseconds, memgraph::utils::MemoryResource *memory) noexcept
@ -194,10 +194,10 @@ struct mgp_local_time {
// have everything noexcept here.
static_assert(std::is_nothrow_copy_constructible_v<memgraph::utils::LocalTime>);
mgp_local_time(const std::string_view string, memgraph::utils::MemoryResource *memory) noexcept
mgp_local_time(const std::string_view string, memgraph::utils::MemoryResource *memory)
: memory(memory), local_time(memgraph::utils::ParseLocalTimeParameters(string).first) {}
mgp_local_time(const mgp_local_time_parameters *parameters, memgraph::utils::MemoryResource *memory) noexcept
mgp_local_time(const mgp_local_time_parameters *parameters, memgraph::utils::MemoryResource *memory)
: memory(memory), local_time(MapLocalTimeParameters(parameters)) {}
mgp_local_time(const memgraph::utils::LocalTime &local_time, memgraph::utils::MemoryResource *memory) noexcept
@ -250,8 +250,7 @@ struct mgp_local_date_time {
mgp_local_date_time(const std::string_view string, memgraph::utils::MemoryResource *memory) noexcept
: memory(memory), local_date_time(CreateLocalDateTimeFromString(string)) {}
mgp_local_date_time(const mgp_local_date_time_parameters *parameters,
memgraph::utils::MemoryResource *memory) noexcept
mgp_local_date_time(const mgp_local_date_time_parameters *parameters, memgraph::utils::MemoryResource *memory)
: memory(memory),
local_date_time(MapDateParameters(parameters->date_parameters),
MapLocalTimeParameters(parameters->local_time_parameters)) {}
@ -301,7 +300,7 @@ struct mgp_duration {
// have everything noexcept here.
static_assert(std::is_nothrow_copy_constructible_v<memgraph::utils::Duration>);
mgp_duration(const std::string_view string, memgraph::utils::MemoryResource *memory) noexcept
mgp_duration(const std::string_view string, memgraph::utils::MemoryResource *memory)
: memory(memory), duration(memgraph::utils::ParseDurationParameters(string)) {}
mgp_duration(const mgp_duration_parameters *parameters, memgraph::utils::MemoryResource *memory) noexcept

View File

@ -109,9 +109,11 @@ void Storage::ReplicationClient::InitializeClient() {
}
if (branching_point) {
spdlog::error(
"Replica {} cannot be used with this instance. Please start a clean "
"instance of Memgraph server on the specified endpoint.",
name_);
"You cannot register Replica {} to this Main because at one point "
"Replica {} acted as the Main instance. Both the Main and Replica {} "
"now hold unique data. Please resolve data conflicts and start the "
"replication on a clean instance.",
name_, name_, name_);
return;
}

View File

@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd.
// Copyright 2023 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@ -51,6 +51,7 @@
M(CartesianOperator, "Number of times Cartesian operator was used.") \
M(CallProcedureOperator, "Number of times CallProcedure operator was used.") \
M(ForeachOperator, "Number of times Foreach operator was used.") \
M(EvaluatePatternFilterOperator, "Number of times EvaluatePatternFilter operator was used.") \
\
M(FailedQuery, "Number of times executing a query failed.") \
M(LabelIndexCreated, "Number of times a label index was created.") \

View File

@ -44,6 +44,7 @@ add_subdirectory(module_file_manager)
add_subdirectory(monitoring_server)
add_subdirectory(lba_procedures)
add_subdirectory(python_query_modules_reloading)
add_subdirectory(mock_api)
copy_e2e_python_files(pytest_runner pytest_runner.sh "")
file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/memgraph-selfsigned.crt DESTINATION ${CMAKE_CURRENT_BINARY_DIR})

View File

@ -0,0 +1,8 @@
function(copy_mock_python_api_e2e_files FILE_NAME)
copy_e2e_python_files(mock_python_api ${FILE_NAME})
endfunction()
add_subdirectory(procedures)
copy_mock_python_api_e2e_files(common.py)
copy_mock_python_api_e2e_files(test_compare_mock.py)

View File

@ -0,0 +1,14 @@
import typing
import mgclient
def connect(**kwargs) -> mgclient.Connection:
connection = mgclient.connect(host="localhost", port=7687, **kwargs)
connection.autocommit = True
return connection
def execute_and_fetch_results_dict(cursor, query) -> typing.Dict:
cursor.execute(query)
return cursor.fetchall()[0][0]

View File

@ -0,0 +1,10 @@
copy_mock_python_api_e2e_files(test_utils.py)
copy_mock_python_api_e2e_files(edge_type.py)
copy_mock_python_api_e2e_files(edge.py)
copy_mock_python_api_e2e_files(graph.py)
copy_mock_python_api_e2e_files(label.py)
copy_mock_python_api_e2e_files(path.py)
copy_mock_python_api_e2e_files(properties.py)
copy_mock_python_api_e2e_files(record.py)
copy_mock_python_api_e2e_files(vertex.py)
copy_mock_python_api_e2e_files(vertices.py)

View File

@ -0,0 +1,85 @@
import mgp
import mgp_mock
import test_utils
@mgp.read_proc
def compare_apis(ctx: mgp.ProcCtx) -> mgp.Record(results_dict=mgp.Map):
mock_ctx = test_utils.get_mock_proc_ctx(is_write=False)
results = dict()
TARGET_EDGE_1_ID = 9
TARGET_EDGE_2_ID = 37
target_edge_1 = test_utils.get_edge(ctx, permanent_id=TARGET_EDGE_1_ID)
target_edge_2 = test_utils.get_edge(ctx, permanent_id=TARGET_EDGE_2_ID)
target_mock_edge_1 = test_utils.get_mock_edge(mock_ctx, id=TARGET_EDGE_1_ID)
target_mock_edge_2 = test_utils.get_mock_edge(mock_ctx, id=TARGET_EDGE_2_ID)
results["is_valid"] = test_utils.all_equal(
target_edge_1.is_valid(),
target_mock_edge_1.is_valid(),
True,
)
results["underlying_graph_is_mutable"] = test_utils.all_equal(
target_edge_1.underlying_graph_is_mutable(),
target_mock_edge_1.underlying_graph_is_mutable(),
False,
)
results["id"] = test_utils.all_equal(
isinstance(target_edge_1.id, int),
isinstance(target_mock_edge_1.id, int),
True,
)
results["type"] = test_utils.all_equal(
target_edge_1.type.name,
target_mock_edge_1.type.name,
"HAS_TEAM",
)
results["from_vertex"] = test_utils.all_equal(
isinstance(target_edge_1.from_vertex, mgp.Vertex),
isinstance(target_mock_edge_1.from_vertex, mgp_mock.Vertex),
True,
)
results["to_vertex"] = test_utils.all_equal(
isinstance(target_edge_1.to_vertex, mgp.Vertex),
isinstance(target_mock_edge_1.to_vertex, mgp_mock.Vertex),
True,
)
results["properties"] = test_utils.all_equal(
isinstance(target_edge_1.properties, mgp.Properties),
isinstance(target_mock_edge_1.properties, mgp_mock.Properties),
True,
) and test_utils.all_equal(
{prop.name: prop.value for prop in target_edge_1.properties.items()},
{prop.name: prop.value for prop in target_mock_edge_1.properties.items()},
{"permanent_id": 9},
)
results["__eq__"] = test_utils.all_equal(
target_edge_1 == target_edge_1,
target_mock_edge_1 == target_mock_edge_1,
True,
) and test_utils.all_equal(
target_edge_1 != target_edge_1,
target_mock_edge_1 != target_mock_edge_1,
False,
)
results["__ne__"] = test_utils.all_equal(
target_edge_1 != target_edge_2,
target_mock_edge_1 != target_mock_edge_2,
True,
) and test_utils.all_equal(
target_edge_1 == target_edge_2,
target_mock_edge_1 == target_mock_edge_2,
False,
)
return mgp.Record(results_dict=results)

View File

@ -0,0 +1,33 @@
import mgp
import test_utils
@mgp.read_proc
def compare_apis(ctx: mgp.ProcCtx) -> mgp.Record(results_dict=mgp.Map):
mock_ctx = test_utils.get_mock_proc_ctx(is_write=False)
results = dict()
TARGET_EDGE_ID = 0
target_edge_type = test_utils.get_edge(ctx, permanent_id=TARGET_EDGE_ID).type
target_mock_edge_type = test_utils.get_mock_edge(mock_ctx, id=TARGET_EDGE_ID).type
results["name"] = test_utils.all_equal(
target_edge_type.name,
target_mock_edge_type.name,
"IS_PART_OF",
)
results["__eq__"] = test_utils.all_equal(
target_edge_type == target_edge_type,
target_edge_type == "IS_PART_OF",
target_mock_edge_type == target_mock_edge_type,
target_mock_edge_type == "IS_PART_OF",
)
results["__ne__"] = test_utils.all_equal(
target_edge_type != "HAS_TEAM",
target_mock_edge_type != "HAS_TEAM",
)
return mgp.Record(results_dict=results)

View File

@ -0,0 +1,102 @@
import mgp
import mgp_mock
import test_utils
@mgp.write_proc
def compare_apis(ctx: mgp.ProcCtx) -> mgp.Record(results_dict=mgp.Map):
VERTEX_ID = 6
mock_ctx = test_utils.get_mock_proc_ctx(is_write=True)
results = dict()
results["is_valid"] = test_utils.all_equal(
ctx.graph.is_valid(),
mock_ctx.graph.is_valid(),
True,
)
results["get_vertex_by_id"] = test_utils.all_equal(
test_utils.get_vertex(ctx, permanent_id=VERTEX_ID).properties["permanent_id"],
mock_ctx.graph.get_vertex_by_id(VERTEX_ID).properties["permanent_id"],
VERTEX_ID,
)
results["vertices"] = test_utils.all_equal(
len(ctx.graph.vertices),
len(mock_ctx.graph.vertices),
27,
)
results["is_mutable"] = test_utils.all_equal(
ctx.graph.is_mutable(),
mock_ctx.graph.is_mutable(),
True,
)
new_mock_vertex = mock_ctx.graph.create_vertex()
new_mock_vertex_id = new_mock_vertex.id
results["create_vertex"] = test_utils.all_equal(
new_mock_vertex_id in [v.id for v in mock_ctx.graph.vertices],
True,
)
mock_ctx.graph.delete_vertex(new_mock_vertex)
results["delete_vertex"] = test_utils.all_equal(
new_mock_vertex_id not in [v.id for v in mock_ctx.graph.vertices],
True,
)
mock_vertex_to_delete = mock_ctx.graph.get_vertex_by_id(VERTEX_ID)
mock_ctx.graph.detach_delete_vertex(mock_vertex_to_delete)
results["detach_delete_vertex"] = test_utils.all_equal(
VERTEX_ID not in [v.properties["permanent_id"] for v in mock_ctx.graph.vertices],
True,
)
MAX_EDGE_ID = 37
START_ID = 10
END1_ID = 13
END2_ID = 14
start_mock_vertex, end1_mock_vertex, end2_mock_vertex = (
mock_ctx.graph.get_vertex_by_id(START_ID),
mock_ctx.graph.get_vertex_by_id(END1_ID),
mock_ctx.graph.get_vertex_by_id(END2_ID),
)
EDGE_TYPE = "CONNECTED_TO"
mock_edge_type = mgp_mock.EdgeType(EDGE_TYPE)
new_mock_edge = mock_ctx.graph.create_edge(start_mock_vertex, end1_mock_vertex, mock_edge_type)
new_mock_edge_id = new_mock_edge.id
results["create_edge"] = test_utils.all_equal(
new_mock_edge_id,
MAX_EDGE_ID + 1,
)
mock_ctx.graph.delete_edge(new_mock_edge)
results["delete_edge"] = test_utils.all_equal(
new_mock_edge_id not in [e.id for e in start_mock_vertex.out_edges],
True,
)
another_mock_edge = mock_ctx.graph.create_edge(start_mock_vertex, end2_mock_vertex, mock_edge_type)
results["edge_id_assignment"] = test_utils.all_equal(
another_mock_edge.id,
MAX_EDGE_ID + 2,
)
return mgp.Record(results_dict=results)
@mgp.read_proc
def test_read_proc_mutability(ctx: mgp.ProcCtx) -> mgp.Record(results_dict=mgp.Map):
mock_ctx = test_utils.get_mock_proc_ctx(is_write=False)
results = dict()
results["is_not_mutable"] = test_utils.all_equal(
ctx.graph.is_mutable(),
mock_ctx.graph.is_mutable(),
False,
)
return mgp.Record(results_dict=results)

View File

@ -0,0 +1,52 @@
import mgp
import test_utils
@mgp.read_proc
def compare_apis(ctx: mgp.ProcCtx) -> mgp.Record(results_dict=mgp.Map):
mock_ctx = test_utils.get_mock_proc_ctx(is_write=False)
results = dict()
TARGET_LABELLED_NODE_ID = 5
target_vertex = test_utils.get_vertex(ctx, permanent_id=TARGET_LABELLED_NODE_ID)
target_mock_vertex = mock_ctx.graph.get_vertex_by_id(TARGET_LABELLED_NODE_ID)
label_1, label_2 = sorted(target_vertex.labels, key=lambda l: l.name) # ("Company", "Startup")
mock_label_1, mock_label_2 = sorted(target_mock_vertex.labels, key=lambda l: l.name) # ditto
results["name"] = test_utils.all_equal(
(label_1.name, label_2.name),
(mock_label_1.name, mock_label_2.name),
("Company", "Startup"),
)
results["__eq__"] = test_utils.all_equal(
label_1 == label_1,
label_1 == "Company",
mock_label_1 == mock_label_1,
mock_label_1 == "Company",
True,
) and test_utils.all_equal(
label_1 == label_2,
label_1 == "Startup",
mock_label_1 == mock_label_2,
mock_label_1 == "Startup",
False,
)
results["__ne__"] = test_utils.all_equal(
label_1 != label_2,
label_1 != "Startup",
mock_label_1 != mock_label_2,
mock_label_1 != "Startup",
True,
) and test_utils.all_equal(
label_1 != label_1,
label_1 != "Company",
mock_label_1 != mock_label_1,
mock_label_1 != "Company",
False,
)
return mgp.Record(results_dict=results)

View File

@ -0,0 +1,65 @@
import copy
import mgp
import mgp_mock
import test_utils
@mgp.read_proc
def compare_apis(ctx: mgp.ProcCtx) -> mgp.Record(results_dict=mgp.Map):
mock_ctx = test_utils.get_mock_proc_ctx(is_write=False)
results = dict()
START_ID = 0
start_vertex = test_utils.get_vertex(ctx, permanent_id=START_ID)
mock_start_vertex = mock_ctx.graph.get_vertex_by_id(START_ID)
path = mgp.Path(start_vertex)
mock_path = mgp_mock.Path(mock_start_vertex)
results["is_valid"] = test_utils.all_equal(
path.is_valid(),
mock_path.is_valid(),
True,
)
EDGE_ID = 0
edge_to_add = test_utils.get_edge(ctx, permanent_id=EDGE_ID)
mock_edge_to_add = test_utils.get_mock_edge(mock_ctx, id=EDGE_ID)
path.expand(edge_to_add)
mock_path.expand(mock_edge_to_add)
results["expand"] = test_utils.all_equal(
(len(path.vertices), len(path.edges)),
(len(mock_path.vertices), len(mock_path.edges)),
(2, 1),
)
NEXT_ID = 1
results["vertices"] = test_utils.all_equal(
all(isinstance(vertex, mgp.Vertex) for vertex in path.vertices),
all(isinstance(vertex, mgp_mock.Vertex) for vertex in mock_path.vertices),
True,
) and test_utils.all_equal(
[vertex.properties["permanent_id"] for vertex in path.vertices],
[vertex.properties["permanent_id"] for vertex in mock_path.vertices],
[START_ID, NEXT_ID],
)
results["edges"] = test_utils.all_equal(
all(isinstance(edge, mgp.Edge) for edge in path.edges),
all(isinstance(edge, mgp_mock.Edge) for edge in mock_path.edges),
True,
) and test_utils.all_equal(
[edge.properties["permanent_id"] for edge in path.edges],
[edge.properties["permanent_id"] for edge in mock_path.edges],
[0],
)
path_copy = copy.copy(path)
mock_path_copy = copy.copy(mock_path)
results["__copy__"] = test_utils.all_equal(
path_copy.is_valid(),
mock_path_copy.is_valid(),
True,
)
return mgp.Record(results_dict=results)

View File

@ -0,0 +1,184 @@
import mgp
import test_utils
@mgp.write_proc
def compare_apis_on_vertex(ctx: mgp.ProcCtx) -> mgp.Record(results_dict=mgp.Map):
mock_ctx = test_utils.get_mock_proc_ctx(is_write=True)
results = dict()
TARGET_ID = 0
target_vertex = test_utils.get_vertex(ctx, permanent_id=TARGET_ID)
target_mock_vertex = mock_ctx.graph.get_vertex_by_id(TARGET_ID)
properties = target_vertex.properties
mock_properties = target_mock_vertex.properties
results["get"] = test_utils.all_equal(
properties.get("name"),
mock_properties.get("name"),
"Peter",
)
results["get[default]"] = test_utils.all_equal(
properties.get("YoE", default="N/A"),
mock_properties.get("YoE", default="N/A"),
"N/A",
)
properties.set("education", "PhD")
mock_properties.set("education", "PhD")
results["set"] = test_utils.all_equal(
properties.get("education"),
mock_properties.get("education"),
"PhD",
)
results["items"] = test_utils.all_equal(
{prop.name: prop.value for prop in properties.items()},
{prop.name: prop.value for prop in mock_properties.items()},
{"name": "Peter", "surname": "Yang", "education": "PhD", "permanent_id": 0},
)
results["keys"] = test_utils.all_equal(
{key for key in properties.keys()},
{key for key in mock_properties.keys()},
{"name", "surname", "education", "permanent_id"},
)
results["values"] = test_utils.all_equal(
{val for val in properties.values()},
{val for val in mock_properties.values()},
{"Peter", "Yang", "PhD", 0},
)
results["__len__"] = test_utils.all_equal(
len(properties),
len(mock_properties),
4,
)
results["__iter__"] = test_utils.all_equal(
{name for name in properties},
{name for name in mock_properties},
{"name", "surname", "education", "permanent_id"},
)
results["__getitem__"] = test_utils.all_equal(
{properties[name] for name in properties},
{mock_properties[name] for name in mock_properties},
{"Peter", "Yang", "PhD", 0},
)
properties["YoE"] = 6
mock_properties["YoE"] = 6
results["__setitem__"] = test_utils.all_equal(
properties["YoE"],
mock_properties["YoE"],
6,
)
results["__contains__"] = test_utils.all_equal(
"YoE" in properties,
"age" not in properties,
"YoE" in mock_properties,
"age" not in mock_properties,
True,
) and test_utils.all_equal(
"YoE" not in properties,
"age" in properties,
"YoE" not in mock_properties,
"age" in mock_properties,
False,
)
return mgp.Record(results_dict=results)
@mgp.write_proc
def compare_apis_on_edge(ctx: mgp.ProcCtx) -> mgp.Record(results_dict=mgp.Map):
mock_ctx = test_utils.get_mock_proc_ctx(is_write=True)
results = dict()
TARGET_EDGE_ID = 37
target_edge_properties = test_utils.get_edge(ctx, permanent_id=TARGET_EDGE_ID).properties
target_mock_edge_properties = test_utils.get_mock_edge(mock_ctx, id=TARGET_EDGE_ID).properties
results["get"] = test_utils.all_equal(
target_edge_properties.get("importance"),
target_mock_edge_properties.get("importance"),
"HIGH",
)
results["get[default]"] = test_utils.all_equal(
target_edge_properties.get("priority", default="N/A"),
target_mock_edge_properties.get("priority", default="N/A"),
"N/A",
)
target_edge_properties.set("priority", "MEDIUM")
target_mock_edge_properties.set("priority", "MEDIUM")
results["set"] = test_utils.all_equal(
target_edge_properties.get("priority"),
target_mock_edge_properties.get("priority"),
"MEDIUM",
)
results["items"] = test_utils.all_equal(
{prop.name: prop.value for prop in target_edge_properties.items()},
{prop.name: prop.value for prop in target_mock_edge_properties.items()},
{"importance": "HIGH", "priority": "MEDIUM", "permanent_id": 37},
)
results["keys"] = test_utils.all_equal(
{key for key in target_edge_properties.keys()},
{key for key in target_mock_edge_properties.keys()},
{"importance", "priority", "permanent_id"},
)
results["values"] = test_utils.all_equal(
{val for val in target_edge_properties.values()},
{val for val in target_mock_edge_properties.values()},
{"HIGH", "MEDIUM", 37},
)
results["__len__"] = test_utils.all_equal(
len(target_edge_properties),
len(target_mock_edge_properties),
3,
)
results["__iter__"] = test_utils.all_equal(
{name for name in target_edge_properties},
{name for name in target_mock_edge_properties},
{"importance", "priority", "permanent_id"},
)
results["__getitem__"] = test_utils.all_equal(
{target_edge_properties[name] for name in target_edge_properties},
{target_mock_edge_properties[name] for name in target_mock_edge_properties},
{"HIGH", "MEDIUM", 37},
)
target_edge_properties["priority"] = "LOW"
target_mock_edge_properties["priority"] = "LOW"
results["__setitem__"] = test_utils.all_equal(
target_edge_properties["priority"],
target_mock_edge_properties["priority"],
"LOW",
)
results["__contains__"] = test_utils.all_equal(
"priority" in target_edge_properties,
"status" not in target_edge_properties,
"priority" in target_mock_edge_properties,
"status" not in target_mock_edge_properties,
True,
) and test_utils.all_equal(
"priority" not in target_edge_properties,
"status" in target_edge_properties,
"priority" not in target_mock_edge_properties,
"status" in target_mock_edge_properties,
False,
)
return mgp.Record(results_dict=results)

View File

@ -0,0 +1,18 @@
import mgp
import mgp_mock
import test_utils
@mgp.read_proc
def compare_apis(ctx: mgp.ProcCtx) -> mgp.Record(results_dict=mgp.Map):
results = dict()
record = mgp.Record(a=1, b=2.0, c="3")
mock_record = mgp_mock.Record(a=1, b=2.0, c="3")
results["fields"] = test_utils.all_equal(
record.fields,
mock_record.fields,
)
return mgp.Record(results_dict=results)

View File

@ -0,0 +1,160 @@
from itertools import groupby
import _mgp_mock
import mgp
import mgp_mock
import networkx as nx
def all_equal(*args):
"""Returns True if all the elements are equal to each other
(source: https://docs.python.org/3/library/itertools.html#itertools-recipes)"""
g = groupby(args)
return next(g, True) and not next(g, False)
def get_mock_proc_ctx(is_write: bool) -> mgp_mock.ProcCtx:
GRAPH_DATA = [
(0, 1, 0),
(5, 1, 9),
(5, 1, 37),
(10, 1, 15),
(1, 2, 4),
(1, 3, 5),
(1, 4, 6),
(0, 5, 1),
(10, 5, 16),
(22, 5, 33),
(1, 6, 7),
(6, 7, 12),
(11, 7, 18),
(13, 7, 26),
(26, 7, 35),
(6, 8, 13),
(26, 8, 36),
(0, 9, 2),
(10, 9, 17),
(22, 9, 34),
(1, 11, 8),
(9, 12, 14),
(14, 13, 27),
(0, 14, 3),
(5, 14, 11),
(12, 14, 22),
(13, 15, 23),
(13, 16, 24),
(13, 17, 25),
(11, 18, 19),
(11, 19, 20),
(11, 20, 21),
(5, 21, 10),
(22, 21, 32),
(21, 23, 28),
(21, 24, 29),
(21, 25, 30),
(21, 26, 31),
]
NODE_INFO = {
0: {"labels": "Person", "name": "Peter", "surname": "Yang", "permanent_id": 0},
1: {"labels": "Team", "name": "Engineering", "permanent_id": 1},
2: {"labels": "Repository", "name": "Memgraph", "permanent_id": 2},
3: {"labels": "Repository", "name": "MAGE", "permanent_id": 3},
4: {"labels": "Repository", "name": "GQLAlchemy", "permanent_id": 4},
5: {"labels": "Company:Startup", "name": "Memgraph", "permanent_id": 5},
6: {"labels": "File", "name": "welcome_to_engineering.txt", "permanent_id": 6},
7: {"labels": "Storage", "name": "Google Drive", "permanent_id": 7},
8: {"labels": "Storage", "name": "Notion", "permanent_id": 8},
9: {"labels": "File", "name": "welcome_to_memgraph.txt", "permanent_id": 9},
10: {"labels": "Person", "name": "Carl", "permanent_id": 10},
11: {"labels": "Folder", "name": "engineering_folder", "permanent_id": 11},
12: {"labels": "Person", "name": "Anna", "permanent_id": 12},
13: {"labels": "Folder", "name": "operations_folder", "permanent_id": 13},
14: {"labels": "Team", "name": "Operations", "permanent_id": 14},
15: {"labels": "File", "name": "operations101.txt", "permanent_id": 15},
16: {"labels": "File", "name": "expenses2022.csv", "permanent_id": 16},
17: {"labels": "File", "name": "salaries2022.csv", "permanent_id": 17},
18: {"labels": "File", "name": "engineering101.txt", "permanent_id": 18},
19: {"labels": "File", "name": "working_with_github.txt", "permanent_id": 19},
20: {"labels": "File", "name": "working_with_notion.txt", "permanent_id": 20},
21: {"labels": "Team", "name": "Marketing", "permanent_id": 21},
22: {"labels": "Person", "name": "Julie", "permanent_id": 22},
23: {"labels": "Account", "name": "Facebook", "permanent_id": 23},
24: {"labels": "Account", "name": "LinkedIn", "permanent_id": 24},
25: {"labels": "Account", "name": "HackerNews", "permanent_id": 25},
26: {"labels": "File", "name": "welcome_to_marketing.txt", "permanent_id": 26},
}
EDGE_INFO = {
(0, 1, 0): {"type": "IS_PART_OF", "permanent_id": 0},
(0, 5, 1): {"type": "IS_PART_OF", "permanent_id": 1},
(0, 9, 2): {"type": "HAS_ACCESS_TO", "permanent_id": 2},
(0, 14, 3): {"type": "IS_PART_OF", "permanent_id": 3},
(1, 2, 4): {"type": "HAS_ACCESS_TO", "permanent_id": 4},
(1, 3, 5): {"type": "HAS_ACCESS_TO", "permanent_id": 5},
(1, 4, 6): {"type": "HAS_ACCESS_TO", "permanent_id": 6},
(1, 6, 7): {"type": "HAS_ACCESS_TO", "permanent_id": 7},
(1, 11, 8): {"type": "HAS_ACCESS_TO", "permanent_id": 8},
(5, 1, 9): {"type": "HAS_TEAM", "permanent_id": 9},
(5, 1, 37): {"type": "HAS_TEAM_2", "importance": "HIGH", "permanent_id": 37},
(5, 14, 11): {"type": "HAS_TEAM", "permanent_id": 11},
(5, 21, 10): {"type": "HAS_TEAM", "permanent_id": 10},
(6, 7, 12): {"type": "IS_STORED_IN", "permanent_id": 12},
(6, 8, 13): {"type": "IS_STORED_IN", "permanent_id": 13},
(9, 12, 14): {"type": "CREATED_BY", "permanent_id": 14},
(10, 1, 15): {"type": "IS_PART_OF", "permanent_id": 15},
(10, 5, 16): {"type": "IS_PART_OF", "permanent_id": 16},
(10, 9, 17): {"type": "HAS_ACCESS_TO", "permanent_id": 17},
(11, 7, 18): {"type": "IS_STORED_IN", "permanent_id": 18},
(11, 18, 19): {"type": "HAS_ACCESS_TO", "permanent_id": 19},
(11, 19, 20): {"type": "HAS_ACCESS_TO", "permanent_id": 20},
(11, 20, 21): {"type": "HAS_ACCESS_TO", "permanent_id": 21},
(12, 14, 22): {"type": "IS_PART_OF", "permanent_id": 22},
(13, 7, 26): {"type": "IS_STORED_IN", "permanent_id": 26},
(13, 15, 23): {"type": "HAS_ACCESS_TO", "permanent_id": 23},
(13, 16, 24): {"type": "HAS_ACCESS_TO", "permanent_id": 24},
(13, 17, 25): {"type": "HAS_ACCESS_TO", "permanent_id": 25},
(14, 13, 27): {"type": "HAS_ACCESS_TO", "permanent_id": 27},
(21, 23, 28): {"type": "HAS_ACCESS_TO", "permanent_id": 28},
(21, 24, 29): {"type": "HAS_ACCESS_TO", "permanent_id": 29},
(21, 25, 30): {"type": "HAS_ACCESS_TO", "permanent_id": 30},
(21, 26, 31): {"type": "HAS_ACCESS_TO", "permanent_id": 31},
(22, 5, 33): {"type": "IS_PART_OF", "permanent_id": 33},
(22, 9, 34): {"type": "HAS_ACCESS_TO", "permanent_id": 34},
(22, 21, 32): {"type": "IS_PART_OF", "permanent_id": 32},
(26, 7, 35): {"type": "IS_STORED_IN", "permanent_id": 35},
(26, 8, 36): {"type": "IS_STORED_IN", "permanent_id": 36},
}
example_graph = nx.MultiDiGraph(GRAPH_DATA)
nx.set_node_attributes(example_graph, NODE_INFO)
nx.set_edge_attributes(example_graph, EDGE_INFO)
if not is_write:
example_graph = nx.freeze(example_graph)
return mgp_mock.ProcCtx(_mgp_mock.Graph(example_graph))
def get_vertex(ctx, permanent_id: int) -> mgp.Vertex:
for vertex in ctx.graph.vertices:
if vertex.properties["permanent_id"] == permanent_id:
return vertex
return None
def get_edge(ctx: mgp.ProcCtx, permanent_id: int) -> mgp.Edge:
for vertex in ctx.graph.vertices:
for edge in vertex.out_edges:
if edge.properties["permanent_id"] == permanent_id:
return edge
return None
def get_mock_edge(ctx: mgp_mock.ProcCtx, id: int) -> mgp_mock.Edge:
for vertex in ctx.graph.vertices:
for edge in vertex.out_edges:
if edge.id == id:
return edge
return None

View File

@ -0,0 +1,101 @@
import typing
import mgp
import mgp_mock
import test_utils
@mgp.read_proc
def compare_apis(ctx: mgp.ProcCtx) -> mgp.Record(results_dict=mgp.Map):
mock_ctx = test_utils.get_mock_proc_ctx(is_write=False)
results = dict()
ID = 1
target_vertex = test_utils.get_vertex(ctx, permanent_id=ID)
target_mock_vertex = mock_ctx.graph.get_vertex_by_id(ID)
results["is_valid"] = test_utils.all_equal(
target_vertex.is_valid(),
target_mock_vertex.is_valid(),
True,
)
results["underlying_graph_is_mutable"] = test_utils.all_equal(
target_vertex.underlying_graph_is_mutable(),
target_mock_vertex.underlying_graph_is_mutable(),
False,
)
results["id"] = test_utils.all_equal(
isinstance(target_vertex.id, int),
isinstance(target_mock_vertex.id, int),
True,
)
results["labels"] = test_utils.all_equal(
isinstance(target_vertex.labels, typing.Tuple),
isinstance(target_mock_vertex.labels, typing.Tuple),
True,
) and test_utils.all_equal(
{label.name for label in target_vertex.labels},
{mock_label.name for mock_label in target_mock_vertex.labels},
{"Team"},
)
results["properties"] = test_utils.all_equal(
isinstance(target_vertex.properties, mgp.Properties),
isinstance(target_mock_vertex.properties, mgp_mock.Properties),
True,
) and test_utils.all_equal(
{prop for prop in target_vertex.properties},
{mock_prop for mock_prop in target_mock_vertex.properties},
{"name", "permanent_id"},
)
results["in_edges"] = test_utils.all_equal(
all(isinstance(edge, mgp.Edge) for edge in target_vertex.in_edges),
all(isinstance(edge, mgp_mock.Edge) for edge in target_mock_vertex.in_edges),
True,
) and test_utils.all_equal(
{edge.properties["permanent_id"] for edge in target_vertex.in_edges},
{edge.properties["permanent_id"] for edge in target_mock_vertex.in_edges},
{0, 9, 15, 37},
)
results["out_edges"] = test_utils.all_equal(
all(isinstance(edge, mgp.Edge) for edge in target_vertex.out_edges),
all(isinstance(edge, mgp_mock.Edge) for edge in target_mock_vertex.out_edges),
True,
) and test_utils.all_equal(
{edge.properties["permanent_id"] for edge in target_vertex.out_edges},
{edge.properties["permanent_id"] for edge in target_mock_vertex.out_edges},
{4, 5, 6, 7, 8},
)
ID_2 = 2
target_vertex_2 = test_utils.get_vertex(ctx, permanent_id=ID_2)
target_mock_vertex_2 = mock_ctx.graph.get_vertex_by_id(ID_2)
results["__eq__"] = test_utils.all_equal(
target_vertex == target_vertex,
target_mock_vertex == target_mock_vertex,
True,
) and test_utils.all_equal(
target_vertex == target_vertex_2,
target_mock_vertex == target_mock_vertex_2,
False,
)
results["__ne__"] = test_utils.all_equal(
target_vertex != target_vertex_2,
target_mock_vertex != target_mock_vertex_2,
True,
) and test_utils.all_equal(
target_vertex != target_vertex,
target_mock_vertex != target_mock_vertex,
False,
)
return mgp.Record(results_dict=results)

View File

@ -0,0 +1,32 @@
import mgp
import mgp_mock
import test_utils
@mgp.read_proc
def compare_apis(ctx: mgp.ProcCtx) -> mgp.Record(results_dict=mgp.Map):
mock_ctx = test_utils.get_mock_proc_ctx(is_write=False)
results = dict()
vertices = ctx.graph.vertices
mock_vertices = mock_ctx.graph.vertices
results["is_valid"] = test_utils.all_equal(
vertices.is_valid(),
mock_vertices.is_valid(),
True,
)
results["__iter__"] = test_utils.all_equal(
all(isinstance(vertex, mgp.Vertex) for vertex in vertices),
all(isinstance(vertex, mgp_mock.Vertex) for vertex in mock_vertices),
True,
)
results["__len__"] = test_utils.all_equal(
len(vertices),
len(mock_vertices),
27,
)
return mgp.Record(results_dict=results)

View File

@ -0,0 +1,195 @@
import sys
import pytest
from common import connect, execute_and_fetch_results_dict
def test_label():
expected_results = {
"name": True,
"__eq__": True,
"__ne__": True,
}
cursor = connect().cursor()
results = execute_and_fetch_results_dict(
cursor, "CALL label.compare_apis() YIELD results_dict RETURN results_dict;"
)
assert results == expected_results
def test_properties_on_vertex():
expected_results = {
"get": True,
"get[default]": True,
"set": True,
"items": True,
"keys": True,
"values": True,
"__len__": True,
"__iter__": True,
"__getitem__": True,
"__setitem__": True,
"__contains__": True,
}
cursor = connect().cursor()
results = execute_and_fetch_results_dict(
cursor, "CALL properties.compare_apis_on_vertex() YIELD results_dict RETURN results_dict;"
)
assert results == expected_results
def test_properties_on_edge():
expected_results = {
"get": True,
"get[default]": True,
"set": True,
"items": True,
"keys": True,
"values": True,
"__len__": True,
"__iter__": True,
"__getitem__": True,
"__setitem__": True,
"__contains__": True,
}
cursor = connect().cursor()
results = execute_and_fetch_results_dict(
cursor, "CALL properties.compare_apis_on_edge() YIELD results_dict RETURN results_dict;"
)
assert results == expected_results
def test_edge_type():
expected_results = {
"name": True,
"__eq__": True,
"__ne__": True,
}
cursor = connect().cursor()
results = execute_and_fetch_results_dict(
cursor, "CALL edge_type.compare_apis() YIELD results_dict RETURN results_dict;"
)
assert results == expected_results
def test_edge():
expected_results = {
"is_valid": True,
"underlying_graph_is_mutable": True,
"id": True,
"type": True,
"from_vertex": True,
"to_vertex": True,
"properties": True,
"__eq__": True,
"__ne__": True,
}
cursor = connect().cursor()
results = execute_and_fetch_results_dict(cursor, "CALL edge.compare_apis() YIELD results_dict RETURN results_dict;")
assert results == expected_results
def test_vertex():
expected_results = {
"is_valid": True,
"underlying_graph_is_mutable": True,
"id": True,
"labels": True,
"properties": True,
"in_edges": True,
"out_edges": True,
"__eq__": True,
"__ne__": True,
}
cursor = connect().cursor()
results = execute_and_fetch_results_dict(
cursor, "CALL vertex.compare_apis() YIELD results_dict RETURN results_dict;"
)
assert results == expected_results
def test_path():
expected_results = {
"__copy__": True,
"is_valid": True,
"expand": True,
"vertices": True,
"edges": True,
}
cursor = connect().cursor()
results = execute_and_fetch_results_dict(cursor, "CALL path.compare_apis() YIELD results_dict RETURN results_dict;")
assert results == expected_results
def test_record():
expected_results = {
"fields": True,
}
cursor = connect().cursor()
results = execute_and_fetch_results_dict(
cursor, "CALL record.compare_apis() YIELD results_dict RETURN results_dict;"
)
assert results == expected_results
def test_vertices():
expected_results = {
"is_valid": True,
"__iter__": True,
"__len__": True,
}
cursor = connect().cursor()
results = execute_and_fetch_results_dict(
cursor, "CALL vertices.compare_apis() YIELD results_dict RETURN results_dict;"
)
assert results == expected_results
def test_graph():
expected_results = {
"create_edge": True,
"create_vertex": True,
"delete_edge": True,
"delete_vertex": True,
"detach_delete_vertex": True,
"edge_id_assignment": True,
"get_vertex_by_id": True,
"is_mutable": True,
"is_not_mutable": True,
"is_valid": True,
"vertices": True,
}
cursor = connect().cursor()
results = execute_and_fetch_results_dict(
cursor, "CALL graph.compare_apis() YIELD results_dict RETURN results_dict;"
)
results.update(
execute_and_fetch_results_dict(
cursor, "CALL graph.test_read_proc_mutability() YIELD results_dict RETURN results_dict;"
)
)
assert results == expected_results
if __name__ == "__main__":
sys.exit(pytest.main([__file__, "-rA"]))

View File

@ -0,0 +1,83 @@
compare_mock: &compare_mock
cluster:
main:
args: ["--bolt-port", "7687", "--log-level=TRACE", "--also-log-to-stderr"]
log_file: "test-compare-mock-e2e.log"
setup_queries:
- "CREATE INDEX ON :__mg_vertex__(__mg_id__);"
- "CREATE (:__mg_vertex__:`Person` {__mg_id__: 0, `name`: 'Peter', `surname`: 'Yang'});"
- "CREATE (:__mg_vertex__:`Team` {__mg_id__: 1, `name`: 'Engineering'});"
- "CREATE (:__mg_vertex__:`Repository` {__mg_id__: 2, `name`: 'Memgraph'});"
- "CREATE (:__mg_vertex__:`Repository` {__mg_id__: 3, `name`: 'MAGE'});"
- "CREATE (:__mg_vertex__:`Repository` {__mg_id__: 4, `name`: 'GQLAlchemy'});"
- "CREATE (:__mg_vertex__:`Company`:`Startup` {__mg_id__: 5, `name`: 'Memgraph'});"
- "CREATE (:__mg_vertex__:`File` {__mg_id__: 6, `name`: 'welcome_to_engineering.txt'});"
- "CREATE (:__mg_vertex__:`Storage` {__mg_id__: 7, `name`: 'Google Drive'});"
- "CREATE (:__mg_vertex__:`Storage` {__mg_id__: 8, `name`: 'Notion'});"
- "CREATE (:__mg_vertex__:`File` {__mg_id__: 9, `name`: 'welcome_to_memgraph.txt'});"
- "CREATE (:__mg_vertex__:`Person` {__mg_id__: 10, `name`: 'Carl'});"
- "CREATE (:__mg_vertex__:`Folder` {__mg_id__: 11, `name`: 'engineering_folder'});"
- "CREATE (:__mg_vertex__:`Person` {__mg_id__: 12, `name`: 'Anna'});"
- "CREATE (:__mg_vertex__:`Folder` {__mg_id__: 13, `name`: 'operations_folder'});"
- "CREATE (:__mg_vertex__:`Team` {__mg_id__: 14, `name`: 'Operations'});"
- "CREATE (:__mg_vertex__:`File` {__mg_id__: 15, `name`: 'operations101.txt'});"
- "CREATE (:__mg_vertex__:`File` {__mg_id__: 16, `name`: 'expenses2022.csv'});"
- "CREATE (:__mg_vertex__:`File` {__mg_id__: 17, `name`: 'salaries2022.csv'});"
- "CREATE (:__mg_vertex__:`File` {__mg_id__: 18, `name`: 'engineering101.txt'});"
- "CREATE (:__mg_vertex__:`File` {__mg_id__: 19, `name`: 'working_with_github.txt'});"
- "CREATE (:__mg_vertex__:`File` {__mg_id__: 20, `name`: 'working_with_notion.txt'});"
- "CREATE (:__mg_vertex__:`Team` {__mg_id__: 21, `name`: 'Marketing'});"
- "CREATE (:__mg_vertex__:`Person` {__mg_id__: 22, `name`: 'Julie'});"
- "CREATE (:__mg_vertex__:`Account` {__mg_id__: 23, `name`: 'Facebook'});"
- "CREATE (:__mg_vertex__:`Account` {__mg_id__: 24, `name`: 'LinkedIn'});"
- "CREATE (:__mg_vertex__:`Account` {__mg_id__: 25, `name`: 'HackerNews'});"
- "CREATE (:__mg_vertex__:`File` {__mg_id__: 26, `name`: 'welcome_to_marketing.txt'});"
- "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 0 AND v.__mg_id__ = 1 CREATE (u)-[:`IS_PART_OF` {`permanent_id`: 0}]->(v);"
- "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 0 AND v.__mg_id__ = 5 CREATE (u)-[:`IS_PART_OF` {`permanent_id`: 1}]->(v);"
- "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 0 AND v.__mg_id__ = 9 CREATE (u)-[:`HAS_ACCESS_TO` {`permanent_id`: 2}]->(v);"
- "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 0 AND v.__mg_id__ = 14 CREATE (u)-[:`IS_PART_OF` {`permanent_id`: 3}]->(v);"
- "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 1 AND v.__mg_id__ = 2 CREATE (u)-[:`HAS_ACCESS_TO` {`permanent_id`: 4}]->(v);"
- "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 1 AND v.__mg_id__ = 3 CREATE (u)-[:`HAS_ACCESS_TO` {`permanent_id`: 5}]->(v);"
- "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 1 AND v.__mg_id__ = 4 CREATE (u)-[:`HAS_ACCESS_TO` {`permanent_id`: 6}]->(v);"
- "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 1 AND v.__mg_id__ = 6 CREATE (u)-[:`HAS_ACCESS_TO` {`permanent_id`: 7}]->(v);"
- "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 1 AND v.__mg_id__ = 11 CREATE (u)-[:`HAS_ACCESS_TO` {`permanent_id`: 8}]->(v);"
- "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 5 AND v.__mg_id__ = 1 CREATE (u)-[:`HAS_TEAM` {`permanent_id`: 9}]->(v);"
- "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 5 AND v.__mg_id__ = 21 CREATE (u)-[:`HAS_TEAM` {`permanent_id`: 10}]->(v);"
- "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 5 AND v.__mg_id__ = 14 CREATE (u)-[:`HAS_TEAM` {`permanent_id`: 11}]->(v);"
- "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 6 AND v.__mg_id__ = 7 CREATE (u)-[:`IS_STORED_IN` {`permanent_id`: 12}]->(v);"
- "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 6 AND v.__mg_id__ = 8 CREATE (u)-[:`IS_STORED_IN` {`permanent_id`: 13}]->(v);"
- "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 9 AND v.__mg_id__ = 12 CREATE (u)-[:`CREATED_BY` {`permanent_id`: 14}]->(v);"
- "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 10 AND v.__mg_id__ = 1 CREATE (u)-[:`IS_PART_OF` {`permanent_id`: 15}]->(v);"
- "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 10 AND v.__mg_id__ = 5 CREATE (u)-[:`IS_PART_OF` {`permanent_id`: 16}]->(v);"
- "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 10 AND v.__mg_id__ = 9 CREATE (u)-[:`HAS_ACCESS_TO` {`permanent_id`: 17}]->(v);"
- "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 11 AND v.__mg_id__ = 7 CREATE(u)-[:`IS_STORED_IN` {`permanent_id`: 18}]->(v);"
- "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 11 AND v.__mg_id__ = 18 CREATE (u)-[:`HAS_ACCESS_TO` {`permanent_id`: 19}]->(v);"
- "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 11 AND v.__mg_id__ = 19 CREATE (u)-[:`HAS_ACCESS_TO` {`permanent_id`: 20}]->(v);"
- "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 11 AND v.__mg_id__ = 20 CREATE (u)-[:`HAS_ACCESS_TO` {`permanent_id`: 21}]->(v);"
- "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 12 AND v.__mg_id__ = 14 CREATE (u)-[:`IS_PART_OF` {`permanent_id`: 22}]->(v);"
- "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 13 AND v.__mg_id__ = 15 CREATE (u)-[:`HAS_ACCESS_TO` {`permanent_id`: 23}]->(v);"
- "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 13 AND v.__mg_id__ = 16 CREATE (u)-[:`HAS_ACCESS_TO` {`permanent_id`: 24}]->(v);"
- "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 13 AND v.__mg_id__ = 17 CREATE (u)-[:`HAS_ACCESS_TO` {`permanent_id`: 25}]->(v);"
- "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 13 AND v.__mg_id__ = 7 CREATE (u)-[:`IS_STORED_IN` {`permanent_id`: 26}]->(v);"
- "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 13 CREATE (u)-[:`HAS_ACCESS_TO` {`permanent_id`: 27}]->(v);"
- "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 21 AND v.__mg_id__ = 23 CREATE (u)-[:`HAS_ACCESS_TO` {`permanent_id`: 28}]->(v);"
- "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 21 AND v.__mg_id__ = 24 CREATE (u)-[:`HAS_ACCESS_TO` {`permanent_id`: 29}]->(v);"
- "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 21 AND v.__mg_id__ = 25 CREATE (u)-[:`HAS_ACCESS_TO` {`permanent_id`: 30}]->(v);"
- "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 21 AND v.__mg_id__ = 26 CREATE (u)-[:`HAS_ACCESS_TO` {`permanent_id`: 31}]->(v);"
- "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 22 AND v.__mg_id__ = 21 CREATE (u)-[:`IS_PART_OF` {`permanent_id`: 32}]->(v);"
- "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 22 AND v.__mg_id__ = 5 CREATE (u)-[:`IS_PART_OF` {`permanent_id`: 33}]->(v);"
- "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 22 AND v.__mg_id__ = 9 CREATE (u)-[:`HAS_ACCESS_TO` {`permanent_id`: 34}]->(v);"
- "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 26 AND v.__mg_id__ = 7 CREATE (u)-[:`IS_STORED_IN` {`permanent_id`: 35}]->(v);"
- "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 26 AND v.__mg_id__ = 8 CREATE (u)-[:`IS_STORED_IN` {`permanent_id`: 36}]->(v);"
- "MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 5 AND v.__mg_id__ = 1 CREATE (u)-[:`HAS_TEAM_2` {`importance`: 'HIGH', `permanent_id`: 37}]->(v);"
- "DROP INDEX ON :__mg_vertex__(__mg_id__);"
- "MATCH (u) SET u.permanent_id = u.__mg_id__;"
- "MATCH (u) REMOVE u:__mg_vertex__, u.__mg_id__;"
validation_queries: []
workloads:
- name: "test-compare-mock" # should be the same as the python file
binary: "tests/e2e/pytest_runner.sh"
proc: "tests/e2e/mock_api/procedures/"
args: ["mock_api/test_compare_mock.py"]
<<: *compare_mock

View File

@ -0,0 +1,529 @@
Feature: WHERE exists
Scenario: Test exists with empty edge and node specifiers
Given an empty graph
And having executed:
"""
CREATE (:One {prop:1})-[:TYPE]->(:Two)
"""
When executing query:
"""
MATCH (n:One) WHERE exists((n)-[]-()) RETURN n.prop;
"""
Then the result should be:
| n.prop |
| 1 |
Scenario: Test exists with empty edge and node specifiers return 2 entries
Given an empty graph
And having executed:
"""
CREATE (:One {prop:1})-[:TYPE]->(:Two), (:One {prop: 3})-[:TYPE]->(:Two)
"""
When executing query:
"""
MATCH (n:One) WHERE exists((n)-[]-()) RETURN n.prop ORDER BY n.prop;
"""
Then the result should be:
| n.prop |
| 1 |
| 3 |
Scenario: Test exists with edge specifier
Given an empty graph
And having executed:
"""
CREATE (:One {prop:1})-[:TYPE]->(:Two)
"""
When executing query:
"""
MATCH (n:One) WHERE exists((n)-[:TYPE]-()) RETURN n.prop;
"""
Then the result should be:
| n.prop |
| 1 |
Scenario: Test exists with wrong edge specifier
Given an empty graph
And having executed:
"""
CREATE (:One {prop:1})-[:TYPE]->(:Two)
"""
When executing query:
"""
MATCH (n:One) WHERE exists((n)-[:TYPE2]-()) RETURN n.prop;
"""
Then the result should be empty
Scenario: Test exists with correct edge direction
Given an empty graph
And having executed:
"""
CREATE (:One {prop:1})-[:TYPE]->(:Two)
"""
When executing query:
"""
MATCH (n:One) WHERE exists((n)-[:TYPE]->()) RETURN n.prop;
"""
Then the result should be:
| n.prop |
| 1 |
Scenario: Test exists with wrong edge direction
Given an empty graph
And having executed:
"""
CREATE (:One {prop:1})-[:TYPE]->(:Two)
"""
When executing query:
"""
MATCH (n:One) WHERE exists((n)<-[:TYPE]-()) RETURN n.prop;
"""
Then the result should be empty
Scenario: Test exists with destination node label
Given an empty graph
And having executed:
"""
CREATE (:One {prop:1})-[:TYPE]->(:Two)
"""
When executing query:
"""
MATCH (n:One) WHERE exists((n)-[]->(:Two)) RETURN n.prop;
"""
Then the result should be:
| n.prop |
| 1 |
Scenario: Test exists with wrong destination node label
Given an empty graph
And having executed:
"""
CREATE (:One {prop:1})-[:TYPE]->(:Two)
"""
When executing query:
"""
MATCH (n:One) WHERE exists((n)-[]->(:Three)) RETURN n.prop;
"""
Then the result should be empty
Scenario: Test exists with destination node property
Given an empty graph
And having executed:
"""
CREATE (:One {prop:1})-[:TYPE]->(:Two {prop: 2})
"""
When executing query:
"""
MATCH (n:One) WHERE exists((n)-[]->({prop: 2})) RETURN n.prop;
"""
Then the result should be:
| n.prop |
| 1 |
Scenario: Test exists with wrong destination node property
Given an empty graph
And having executed:
"""
CREATE (:One {prop:1})-[:TYPE]->(:Two {prop: 2})
"""
When executing query:
"""
MATCH (n:One) WHERE exists((n)-[]->({prop: 3})) RETURN n.prop;
"""
Then the result should be empty
Scenario: Test exists with edge property
Given an empty graph
And having executed:
"""
CREATE (:One {prop:1})-[:TYPE {prop: 1}]->(:Two {prop: 2})
"""
When executing query:
"""
MATCH (n:One) WHERE exists((n)-[:TYPE {prop: 1}]->()) RETURN n.prop;
"""
Then the result should be:
| n.prop |
| 1 |
Scenario: Test exists with wrong edge property
Given an empty graph
And having executed:
"""
CREATE (:One {prop:1})-[:TYPE {prop: 1}]->(:Two {prop: 2})
"""
When executing query:
"""
MATCH (n:One) WHERE exists((n)-[:TYPE {prop: 2}]->()) RETURN n.prop;
"""
Then the result should be empty
Scenario: Test exists with both edge property and node label property
Given an empty graph
And having executed:
"""
CREATE (:One {prop:1})-[:TYPE {prop: 1}]->(:Two {prop: 2})
"""
When executing query:
"""
MATCH (n:One) WHERE exists((n)-[:TYPE {prop: 1}]->(:Two {prop: 2})) RETURN n.prop;
"""
Then the result should be:
| n.prop |
| 1 |
Scenario: Test exists with correct edge property and wrong node label property
Given an empty graph
And having executed:
"""
CREATE (:One {prop:1})-[:TYPE {prop: 1}]->(:Two {prop: 2})
"""
When executing query:
"""
MATCH (n:One) WHERE exists((n)-[:TYPE {prop: 1}]->(:Two {prop: 3})) RETURN n.prop;
"""
Then the result should be empty
Scenario: Test exists with wrong edge property and correct node label property
Given an empty graph
And having executed:
"""
CREATE (:One {prop:1})-[:TYPE {prop: 1}]->(:Two {prop: 2})
"""
When executing query:
"""
MATCH (n:One) WHERE exists((n)-[:TYPE {prop: 2}]->(:Two {prop:2})) RETURN n.prop;
"""
Then the result should be empty
Scenario: Test exists with wrong edge property and wrong node label property
Given an empty graph
And having executed:
"""
CREATE (:One {prop:1})-[:TYPE {prop: 1}]->(:Two {prop: 2})
"""
When executing query:
"""
MATCH (n:One) WHERE exists((n)-[:TYPE {prop: 2}]->(:Two {prop:3})) RETURN n.prop;
"""
Then the result should be empty
Scenario: Test exists AND exists
Given an empty graph
And having executed:
"""
CREATE (:One {prop:1})-[:TYPE {prop: 1}]->(:Two {prop: 2})
"""
When executing query:
"""
MATCH (n:One) WHERE exists((n)-[:TYPE]->()) AND exists((n)-[]->(:Two)) RETURN n.prop;
"""
Then the result should be:
| n.prop |
| 1 |
Scenario: Test exists OR exists first condition
Given an empty graph
And having executed:
"""
CREATE (:One {prop:1})-[:TYPE {prop: 1}]->(:Two {prop: 2})
"""
When executing query:
"""
MATCH (n:One) WHERE exists((n)-[:TYPE]->()) OR exists((n)-[]->(:Three)) RETURN n.prop;
"""
Then the result should be:
| n.prop |
| 1 |
Scenario: Test exists OR exists second condition
Given an empty graph
And having executed:
"""
CREATE (:One {prop:1})-[:TYPE {prop: 1}]->(:Two {prop: 2})
"""
When executing query:
"""
MATCH (n:One) WHERE exists((n)-[:TYPE2]->()) OR exists((n)-[]->(:Two)) RETURN n.prop;
"""
Then the result should be:
| n.prop |
| 1 |
Scenario: Test exists OR exists fail
Given an empty graph
And having executed:
"""
CREATE (:One {prop:1})-[:TYPE {prop: 1}]->(:Two {prop: 2})
"""
When executing query:
"""
MATCH (n:One) WHERE exists((n)-[:TYPE2]->()) OR exists((n)-[]->(:Three)) RETURN n.prop;
"""
Then the result should be empty
Scenario: Test NOT exists
Given an empty graph
And having executed:
"""
CREATE (:One {prop:1})-[:TYPE {prop: 1}]->(:Two {prop: 2})
"""
When executing query:
"""
MATCH (n:One) WHERE NOT exists((n)-[:TYPE2]->()) RETURN n.prop;
"""
Then the result should be:
| n.prop |
| 1 |
Scenario: Test multi-hop first in sequence
Given an empty graph
And having executed:
"""
CREATE (:One {prop:1})-[:TYPE {prop: 1}]->(:Two {prop: 2})-[:TYPE {prop:2}]->(:Three {prop: 3})
"""
When executing query:
"""
MATCH (n) WHERE exists((n)-[]->()-[]->()) RETURN n.prop;
"""
Then the result should be:
| n.prop |
| 1 |
Scenario: Test multi-hop in middle sequence
Given an empty graph
And having executed:
"""
CREATE (:One {prop:1})-[:TYPE {prop: 1}]->(:Two {prop: 2})-[:TYPE {prop:2}]->(:Three {prop: 3})
"""
When executing query:
"""
MATCH (n) WHERE exists(()-[]->(n)-[]->()) RETURN n.prop;
"""
Then the result should be:
| n.prop |
| 2 |
Scenario: Test multi-hop at the end of the sequence
Given an empty graph
And having executed:
"""
CREATE (:One {prop:1})-[:TYPE {prop: 1}]->(:Two {prop: 2})-[:TYPE {prop:2}]->(:Three {prop: 3})
"""
When executing query:
"""
MATCH (n) WHERE exists(()-[]->()-[]->(n)) RETURN n.prop;
"""
Then the result should be:
| n.prop |
| 3 |
Scenario: Test multi-hop not exists
Given an empty graph
And having executed:
"""
CREATE (:One {prop:1})-[:TYPE {prop: 1}]->(:Two {prop: 2})-[:TYPE {prop:2}]->(:Three {prop: 3})
"""
When executing query:
"""
MATCH (n) WHERE exists(()-[]->(n)<-[]-()) RETURN n.prop;
"""
Then the result should be empty
Scenario: Test multi-hop with filters
Given an empty graph
And having executed:
"""
CREATE (:One {prop:1})-[:TYPE {prop: 1}]->(:Two {prop: 2})-[:TYPE {prop:2}]->(:Three {prop: 3})
"""
When executing query:
"""
MATCH (n) WHERE exists(({prop: 1})-[:TYPE]->(n)-[{prop:2}]->(:Three)) RETURN n.prop;
"""
Then the result should be:
| n.prop |
| 2 |
Scenario: Test multi-hop with wrong filters
Given an empty graph
And having executed:
"""
CREATE (:One {prop:1})-[:TYPE {prop: 1}]->(:Two {prop: 2})-[:TYPE {prop:2}]->(:Three {prop: 3})
"""
When executing query:
"""
MATCH (n) WHERE exists(({prop: 1})-[:TYPE]->(n)-[:TYPE2]->(:Three)) RETURN n.prop;
"""
Then the result should be empty
Scenario: Test node-only hop
Given an empty graph
And having executed:
"""
CREATE (:One {prop:1})-[:TYPE {prop: 1}]->(:Two {prop: 2})-[:TYPE {prop:2}]->(:Three {prop: 3})
"""
When executing query:
"""
MATCH (n) WHERE exists((n)) RETURN n.prop;
"""
Then the result should be:
| n.prop |
| 1 |
| 2 |
| 3 |
Scenario: Test exists with different edge type
Given an empty graph
And having executed:
"""
CREATE (:One {prop:1})-[:TYPE]->(:Two)
"""
When executing query:
"""
MATCH (n:One) WHERE exists((n)-[:TYPE2]->()) RETURN n.prop;
"""
Then the result should be empty
Scenario: Test exists with correct edge type multiple edges
Given an empty graph
And having executed:
"""
CREATE (:One {prop:1})-[:TYPE]->(:Two {prop: 10}), (:One {prop: 2})-[:TYPE]->(:Two {prop: 11});
"""
When executing query:
"""
MATCH (n:Two) WHERE exists((n)<-[:TYPE]-()) RETURN n.prop ORDER BY n.prop;
"""
Then the result should be:
| n.prop |
| 10 |
| 11 |
Scenario: Test exists does not work in WITH clauses
Given an empty graph
And having executed:
"""
CREATE (:One {prop:1})-[:TYPE]->(:Two);
"""
When executing query:
"""
MATCH (n:Two) WITH n WHERE exists((n)<-[:TYPE]-()) RETURN n.prop;
"""
Then an error should be raised
Scenario: Test exists is not null
Given an empty graph
And having executed:
"""
CREATE (:One {prop:1})-[:TYPE]->(:Two);
"""
When executing query:
"""
MATCH (n:One) WHERE exists((n)-[]-()) is not null RETURN n.prop;
"""
Then the result should be:
| n.prop |
| 1 |
Scenario: Test exists is null
Given an empty graph
And having executed:
"""
CREATE (:One {prop:1})-[:TYPE]->(:Two);
"""
When executing query:
"""
MATCH (n:One) WHERE exists((n)-[]-()) is null RETURN n.prop;
"""
Then the result should be empty
Scenario: Test exists equal to true
Given an empty graph
And having executed:
"""
CREATE (:One {prop:1})-[:TYPE]->(:Two);
"""
When executing query:
"""
MATCH (n:One) WHERE exists((n)-[]-()) = true RETURN n.prop;
"""
Then the result should be:
| n.prop |
| 1 |
Scenario: Test exists equal to true
Given an empty graph
And having executed:
"""
CREATE (:One {prop:1})-[:TYPE]->(:Two);
"""
When executing query:
"""
MATCH (n:One) WHERE exists((n)-[]-()) = false RETURN n.prop;
"""
Then the result should be empty
Scenario: Test exists in list
Given an empty graph
And having executed:
"""
CREATE (:One {prop:1})-[:TYPE]->(:Two);
"""
When executing query:
"""
MATCH (n:One) WHERE exists((n)-[]-()) in [true] RETURN n.prop;
"""
Then the result should be:
| n.prop |
| 1 |
Scenario: Test BFS hop
Given an empty graph
And having executed:
"""
CREATE (:One {prop:1})-[:TYPE {prop: 1}]->(:Two {prop: 2})-[:TYPE {prop:2}]->(:Three {prop: 3})
"""
When executing query:
"""
MATCH (n:One) WHERE exists((n)-[*bfs]->(:Three)) RETURN n.prop;
"""
Then the result should be:
| n.prop |
| 1 |
Scenario: Test exists not in list
Given an empty graph
And having executed:
"""
CREATE (:One {prop:1})-[:TYPE]->(:Two);
"""
When executing query:
"""
MATCH (n:One) WHERE exists((n)-[]-()) in [false] RETURN n.prop;
"""
Then the result should be empty
Scenario: Test exists on multihop patterns without results
Given an empty graph
And having executed:
"""
MATCH (n) DETACH DELETE n;
"""
When executing query:
"""
MATCH ()-[]-(m)-[]->(a) WHERE m.prop=1 and a.prop=3 and exists(()-[]->(m)) RETURN m, a;
"""
Then the result should be empty
Scenario: Test exists does not work in SetProperty clauses
Given an empty graph
And having executed:
"""
CREATE (:One {prop:1})-[:TYPE]->(:Two);
"""
When executing query:
"""
MATCH (n:Two) SET n.prop = exists((n)<-[:TYPE]-()) RETURN n.prop;
"""
Then an error should be raised

View File

@ -15,6 +15,7 @@ PIP_DEPS=(
"pytest==6.2.3"
"pyyaml==5.4.1"
"six==1.15.0"
"networkx==2.4"
)
cd "$DIR"

View File

@ -389,6 +389,10 @@ TEST_F(CppApiTestFixture, TestLocalDateTime) {
auto ldt_1 = mgp::LocalDateTime("2021-10-05T14:15:00");
auto ldt_2 = mgp::LocalDateTime(2021, 10, 5, 14, 15, 0, 0, 0);
ASSERT_ANY_THROW(mgp::LocalDateTime(
2021, 10, 0, 14, 15, 0, 0,
0)); // ...10, 0, 14... <- 0 is an illegal value for the `day` parameter; must throw an exception
ASSERT_EQ(ldt_1.Year(), 2021);
ASSERT_EQ(ldt_1.Month(), 10);
ASSERT_EQ(ldt_1.Day(), 5);

View File

@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd.
// Copyright 2023 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@ -4307,3 +4307,75 @@ TEST_P(CypherMainVisitorTest, Foreach) {
ASSERT_TRUE(dynamic_cast<RemoveProperty *>(*++clauses.begin()));
}
}
TEST_P(CypherMainVisitorTest, ExistsThrow) {
auto &ast_generator = *GetParam();
TestInvalidQueryWithMessage<SyntaxException>("MATCH (n) WHERE exists(p=(n)-[]->()) RETURN n;", ast_generator,
"Identifiers are not supported in exists(...).");
}
TEST_P(CypherMainVisitorTest, Exists) {
auto &ast_generator = *GetParam();
{
const auto *query =
dynamic_cast<CypherQuery *>(ast_generator.ParseQuery("MATCH (n) WHERE exists((n)-[]->()) RETURN n;"));
const auto *match = dynamic_cast<Match *>(query->single_query_->clauses_[0]);
const auto *exists = dynamic_cast<Exists *>(match->where_->expression_);
ASSERT_TRUE(exists);
const auto pattern = exists->pattern_;
ASSERT_TRUE(pattern->atoms_.size() == 3);
const auto *node1 = dynamic_cast<NodeAtom *>(pattern->atoms_[0]);
const auto *edge = dynamic_cast<EdgeAtom *>(pattern->atoms_[1]);
const auto *node2 = dynamic_cast<NodeAtom *>(pattern->atoms_[2]);
ASSERT_TRUE(node1);
ASSERT_TRUE(edge);
ASSERT_TRUE(node2);
}
{
const auto *query =
dynamic_cast<CypherQuery *>(ast_generator.ParseQuery("MATCH (n) WHERE exists((n)-[]->()-[]->()) RETURN n;"));
const auto *match = dynamic_cast<Match *>(query->single_query_->clauses_[0]);
const auto *exists = dynamic_cast<Exists *>(match->where_->expression_);
ASSERT_TRUE(exists);
const auto pattern = exists->pattern_;
ASSERT_TRUE(pattern->atoms_.size() == 5);
const auto *node1 = dynamic_cast<NodeAtom *>(pattern->atoms_[0]);
const auto *edge = dynamic_cast<EdgeAtom *>(pattern->atoms_[1]);
const auto *node2 = dynamic_cast<NodeAtom *>(pattern->atoms_[2]);
const auto *edge2 = dynamic_cast<EdgeAtom *>(pattern->atoms_[3]);
const auto *node3 = dynamic_cast<NodeAtom *>(pattern->atoms_[4]);
ASSERT_TRUE(node1);
ASSERT_TRUE(edge);
ASSERT_TRUE(node2);
ASSERT_TRUE(edge2);
ASSERT_TRUE(node3);
}
{
const auto *query = dynamic_cast<CypherQuery *>(ast_generator.ParseQuery("MATCH (n) WHERE exists((n)) RETURN n;"));
const auto *match = dynamic_cast<Match *>(query->single_query_->clauses_[0]);
const auto *exists = dynamic_cast<Exists *>(match->where_->expression_);
ASSERT_TRUE(exists);
const auto pattern = exists->pattern_;
ASSERT_TRUE(pattern->atoms_.size() == 1);
const auto *node = dynamic_cast<NodeAtom *>(pattern->atoms_[0]);
ASSERT_TRUE(node);
}
}

View File

@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd.
// Copyright 2023 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@ -378,7 +378,8 @@ TEST_F(PrintToJsonTest, ConstructNamedPath) {
TEST_F(PrintToJsonTest, Filter) {
std::shared_ptr<LogicalOperator> last_op = std::make_shared<ScanAll>(nullptr, GetSymbol("node1"));
last_op = std::make_shared<Filter>(last_op, EQ(PROPERTY_LOOKUP("node1", dba.NameToProperty("prop")), LITERAL(5)));
last_op = std::make_shared<Filter>(last_op, std::vector<std::shared_ptr<LogicalOperator>>{},
EQ(PROPERTY_LOOKUP("node1", dba.NameToProperty("prop")), LITERAL(5)));
Check(last_op.get(), R"sep(
{
@ -995,3 +996,55 @@ TEST_F(PrintToJsonTest, Foreach) {
}
})sep");
}
TEST_F(PrintToJsonTest, Exists) {
Symbol x = GetSymbol("x");
Symbol e = GetSymbol("edge");
Symbol n = GetSymbol("node");
Symbol output = GetSymbol("output_symbol");
std::shared_ptr<LogicalOperator> last_op = std::make_shared<ScanAll>(nullptr, x);
std::shared_ptr<LogicalOperator> expand = std::make_shared<Expand>(
nullptr, x, n, e, memgraph::query::EdgeAtom::Direction::BOTH,
std::vector<memgraph::storage::EdgeTypeId>{dba.NameToEdgeType("EdgeType1")}, false, memgraph::storage::View::OLD);
std::shared_ptr<LogicalOperator> limit = std::make_shared<Limit>(expand, LITERAL(1));
std::shared_ptr<LogicalOperator> evaluate_pattern_filter = std::make_shared<EvaluatePatternFilter>(limit, output);
last_op = std::make_shared<Filter>(
last_op, std::vector<std::shared_ptr<LogicalOperator>>{evaluate_pattern_filter},
EXISTS(PATTERN(NODE("x"), EDGE("edge", memgraph::query::EdgeAtom::Direction::BOTH, {}, false),
NODE("node", std::nullopt, false))));
Check(last_op.get(), R"sep(
{
"expression": "(Exists expression)",
"input": {
"input": {
"name": "Once"
},
"name": "ScanAll",
"output_symbol": "x"
},
"name": "Filter",
"pattern_filter1": {
"input": {
"expression": "1",
"input": {
"direction": "both",
"edge_symbol": "edge",
"edge_types": [
"EdgeType1"
],
"existing_node": false,
"input": {
"name": "Once"
},
"input_symbol": "x",
"name": "Expand",
"node_symbol": "node"
},
"name": "Limit"
},
"name": "EvaluatePatternFilter",
"output_symbol": "output_symbol"
}
})sep");
}

View File

@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd.
// Copyright 2023 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@ -157,13 +157,13 @@ auto GetPropertyLookup(AstStorage &storage, TDbAccessor &, Expression *expr,
///
/// Name is used to create the Identifier which is assigned to the edge.
auto GetEdge(AstStorage &storage, const std::string &name, EdgeAtom::Direction dir = EdgeAtom::Direction::BOTH,
const std::vector<std::string> &edge_types = {}) {
const std::vector<std::string> &edge_types = {}, const bool user_declared = true) {
std::vector<EdgeTypeIx> types;
types.reserve(edge_types.size());
for (const auto &type : edge_types) {
types.push_back(storage.GetEdgeTypeIx(type));
}
return storage.Create<EdgeAtom>(storage.Create<Identifier>(name), EdgeAtom::Type::SINGLE, dir, types);
return storage.Create<EdgeAtom>(storage.Create<Identifier>(name, user_declared), EdgeAtom::Type::SINGLE, dir, types);
}
/// Create a variable length expansion EdgeAtom with given name, direction and
@ -205,8 +205,9 @@ auto GetEdgeVariable(AstStorage &storage, const std::string &name, EdgeAtom::Typ
/// Create a NodeAtom with given name and label.
///
/// Name is used to create the Identifier which is assigned to the node.
auto GetNode(AstStorage &storage, const std::string &name, std::optional<std::string> label = std::nullopt) {
auto node = storage.Create<NodeAtom>(storage.Create<Identifier>(name));
auto GetNode(AstStorage &storage, const std::string &name, std::optional<std::string> label = std::nullopt,
const bool user_declared = true) {
auto node = storage.Create<NodeAtom>(storage.Create<Identifier>(name, user_declared));
if (label) node->labels_.emplace_back(storage.GetLabelIx(*label));
return node;
}
@ -586,6 +587,7 @@ auto GetForeach(AstStorage &storage, NamedExpression *named_expr, const std::vec
#define COALESCE(...) storage.Create<memgraph::query::Coalesce>(std::vector<memgraph::query::Expression *>{__VA_ARGS__})
#define EXTRACT(variable, list, expr) \
storage.Create<memgraph::query::Extract>(storage.Create<memgraph::query::Identifier>(variable), list, expr)
#define EXISTS(pattern) storage.Create<memgraph::query::Exists>(pattern)
#define AUTH_QUERY(action, user, role, user_or_role, password, privileges, labels, edgeTypes) \
storage.Create<memgraph::query::AuthQuery>((action), (user), (role), (user_or_role), password, (privileges), \
(labels), (edgeTypes))

View File

@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd.
// Copyright 2023 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@ -204,7 +204,8 @@ TEST_F(QueryCostEstimator, Foreach) {
EXPECT_COST(OP_COST_PARAM + OP_CARD_PARAM * OP_COST_PARAM);
TEST_F(QueryCostEstimator, Filter) {
TEST_OP(MakeOp<Filter>(last_op_, Literal(true)), CostParam::kFilter, CardParam::kFilter);
TEST_OP(MakeOp<Filter>(last_op_, std::vector<std::shared_ptr<LogicalOperator>>{}, Literal(true)), CostParam::kFilter,
CardParam::kFilter);
}
TEST_F(QueryCostEstimator, EdgeUniquenessFilter) {

View File

@ -1735,4 +1735,119 @@ TYPED_TEST(TestPlanner, Foreach) {
DeleteListContent(&on_create);
}
}
TYPED_TEST(TestPlanner, Exists) {
AstStorage storage;
FakeDbAccessor dba;
// MATCH (n) WHERE exists((n)-[]-())
{
auto *query = QUERY(SINGLE_QUERY(
MATCH(PATTERN(NODE("n"))),
WHERE(EXISTS(PATTERN(NODE("n"), EDGE("edge", memgraph::query::EdgeAtom::Direction::BOTH, {}, false),
NODE("node", std::nullopt, false)))),
RETURN("n")));
auto symbol_table = memgraph::query::MakeSymbolTable(query);
auto planner = MakePlanner<TypeParam>(&dba, storage, symbol_table, query);
std::list<BaseOpChecker *> pattern_filter{new ExpectExpand(), new ExpectLimit(), new ExpectEvaluatePatternFilter()};
CheckPlan(planner.plan(), symbol_table, ExpectScanAll(),
ExpectFilter(std::vector<std::list<BaseOpChecker *>>{pattern_filter}), ExpectProduce());
DeleteListContent(&pattern_filter);
}
// MATCH (n) WHERE exists((n)-[:TYPE]-(:Two))
{
auto *query = QUERY(SINGLE_QUERY(
MATCH(PATTERN(NODE("n"))),
WHERE(EXISTS(PATTERN(NODE("n"), EDGE("edge", memgraph::query::EdgeAtom::Direction::BOTH, {"TYPE"}, false),
NODE("node", "Two", false)))),
RETURN("n")));
auto symbol_table = memgraph::query::MakeSymbolTable(query);
auto planner = MakePlanner<TypeParam>(&dba, storage, symbol_table, query);
std::list<BaseOpChecker *> pattern_filter{new ExpectExpand(), new ExpectFilter(), new ExpectLimit(),
new ExpectEvaluatePatternFilter()};
CheckPlan(planner.plan(), symbol_table, ExpectScanAll(),
ExpectFilter(std::vector<std::list<BaseOpChecker *>>{pattern_filter}), ExpectProduce());
DeleteListContent(&pattern_filter);
}
// MATCH (n) WHERE exists((n)-[:TYPE]-(:Two)) AND exists((n)-[]-())
{
auto *query = QUERY(SINGLE_QUERY(
MATCH(PATTERN(NODE("n"))),
WHERE(AND(EXISTS(PATTERN(NODE("n"), EDGE("edge", memgraph::query::EdgeAtom::Direction::BOTH, {"TYPE"}, false),
NODE("node", "Two", false))),
EXISTS(PATTERN(NODE("n"), EDGE("edge2", memgraph::query::EdgeAtom::Direction::BOTH, {}, false),
NODE("node2", std::nullopt, false))))),
RETURN("n")));
auto symbol_table = memgraph::query::MakeSymbolTable(query);
auto planner = MakePlanner<TypeParam>(&dba, storage, symbol_table, query);
std::list<BaseOpChecker *> pattern_filter_with_types{new ExpectExpand(), new ExpectFilter(), new ExpectLimit(),
new ExpectEvaluatePatternFilter()};
std::list<BaseOpChecker *> pattern_filter_without_types{new ExpectExpand(), new ExpectLimit(),
new ExpectEvaluatePatternFilter()};
CheckPlan(
planner.plan(), symbol_table, ExpectScanAll(),
ExpectFilter(std::vector<std::list<BaseOpChecker *>>{pattern_filter_without_types, pattern_filter_with_types}),
ExpectProduce());
DeleteListContent(&pattern_filter_with_types);
DeleteListContent(&pattern_filter_without_types);
}
// MATCH (n) WHERE n.prop = 1 AND exists((n)-[:TYPE]-(:Two))
{
auto property = dba.Property("prop");
auto *query = QUERY(SINGLE_QUERY(
MATCH(PATTERN(NODE("n"))),
WHERE(AND(EXISTS(PATTERN(NODE("n"), EDGE("edge", memgraph::query::EdgeAtom::Direction::BOTH, {"TYPE"}, false),
NODE("node", "Two", false))),
PROPERTY_LOOKUP("n", property))),
RETURN("n")));
auto symbol_table = memgraph::query::MakeSymbolTable(query);
auto planner = MakePlanner<TypeParam>(&dba, storage, symbol_table, query);
std::list<BaseOpChecker *> pattern_filter{new ExpectExpand(), new ExpectFilter(), new ExpectLimit(),
new ExpectEvaluatePatternFilter()};
CheckPlan(planner.plan(), symbol_table, ExpectScanAll(),
ExpectFilter(std::vector<std::list<BaseOpChecker *>>{pattern_filter}), ExpectProduce());
DeleteListContent(&pattern_filter);
}
// MATCH (n) WHERE exists((n)-[:TYPE]-(:Two)) OR exists((n)-[]-())
{
auto *query = QUERY(SINGLE_QUERY(
MATCH(PATTERN(NODE("n"))),
WHERE(OR(EXISTS(PATTERN(NODE("n"), EDGE("edge", memgraph::query::EdgeAtom::Direction::BOTH, {"TYPE"}, false),
NODE("node", "Two", false))),
EXISTS(PATTERN(NODE("n"), EDGE("edge2", memgraph::query::EdgeAtom::Direction::BOTH, {}, false),
NODE("node2", std::nullopt, false))))),
RETURN("n")));
auto symbol_table = memgraph::query::MakeSymbolTable(query);
auto planner = MakePlanner<TypeParam>(&dba, storage, symbol_table, query);
std::list<BaseOpChecker *> pattern_filter_with_types{new ExpectExpand(), new ExpectFilter(), new ExpectLimit(),
new ExpectEvaluatePatternFilter()};
std::list<BaseOpChecker *> pattern_filter_without_types{new ExpectExpand(), new ExpectLimit(),
new ExpectEvaluatePatternFilter()};
CheckPlan(
planner.plan(), symbol_table, ExpectScanAll(),
ExpectFilter(std::vector<std::list<BaseOpChecker *>>{pattern_filter_with_types, pattern_filter_without_types}),
ExpectProduce());
DeleteListContent(&pattern_filter_with_types);
DeleteListContent(&pattern_filter_without_types);
}
}
} // namespace

View File

@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd.
// Copyright 2023 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@ -64,7 +64,6 @@ class PlanChecker : public virtual HierarchicalLogicalOperatorVisitor {
PRE_VISIT(ScanAllById);
PRE_VISIT(Expand);
PRE_VISIT(ExpandVariable);
PRE_VISIT(Filter);
PRE_VISIT(ConstructNamedPath);
PRE_VISIT(EmptyResult);
PRE_VISIT(Produce);
@ -79,6 +78,7 @@ class PlanChecker : public virtual HierarchicalLogicalOperatorVisitor {
PRE_VISIT(Skip);
PRE_VISIT(Limit);
PRE_VISIT(OrderBy);
PRE_VISIT(EvaluatePatternFilter);
bool PreVisit(Merge &op) override {
CheckOp(op);
op.input()->Accept(*this);
@ -97,6 +97,12 @@ class PlanChecker : public virtual HierarchicalLogicalOperatorVisitor {
return false;
}
bool PreVisit(Filter &op) override {
CheckOp(op);
op.input()->Accept(*this);
return false;
}
bool Visit(Once &) override {
// Ignore checking Once, it is implicitly at the end.
return true;
@ -141,7 +147,6 @@ using ExpectScanAll = OpChecker<ScanAll>;
using ExpectScanAllByLabel = OpChecker<ScanAllByLabel>;
using ExpectScanAllById = OpChecker<ScanAllById>;
using ExpectExpand = OpChecker<Expand>;
using ExpectFilter = OpChecker<Filter>;
using ExpectConstructNamedPath = OpChecker<ConstructNamedPath>;
using ExpectProduce = OpChecker<Produce>;
using ExpectEmptyResult = OpChecker<EmptyResult>;
@ -156,6 +161,23 @@ using ExpectLimit = OpChecker<Limit>;
using ExpectOrderBy = OpChecker<OrderBy>;
using ExpectUnwind = OpChecker<Unwind>;
using ExpectDistinct = OpChecker<Distinct>;
using ExpectEvaluatePatternFilter = OpChecker<EvaluatePatternFilter>;
class ExpectFilter : public OpChecker<Filter> {
public:
ExpectFilter(const std::vector<std::list<BaseOpChecker *>> &pattern_filters = {})
: pattern_filters_(pattern_filters) {}
void ExpectOp(Filter &filter, const SymbolTable &symbol_table) override {
for (auto i = 0; i < filter.pattern_filters_.size(); i++) {
PlanChecker check_updates(pattern_filters_[i], symbol_table);
filter.pattern_filters_[i]->Accept(check_updates);
}
}
std::vector<std::list<BaseOpChecker *>> pattern_filters_;
};
class ExpectForeach : public OpChecker<Foreach> {
public:

View File

@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd.
// Copyright 2023 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@ -1532,7 +1532,7 @@ TEST(QueryPlan, NodeFilterSet) {
false, memgraph::storage::View::OLD);
auto *filter_expr =
EQ(storage.Create<PropertyLookup>(scan_all.node_->identifier_, storage.GetPropertyIx(prop.first)), LITERAL(42));
auto node_filter = std::make_shared<Filter>(expand.op_, filter_expr);
auto node_filter = std::make_shared<Filter>(expand.op_, std::vector<std::shared_ptr<LogicalOperator>>{}, filter_expr);
// SET n.prop = n.prop + 1
auto set_prop = PROPERTY_LOOKUP(IDENT("n")->MapTo(scan_all.sym_), prop);
auto add = ADD(set_prop, LITERAL(1));
@ -1569,7 +1569,8 @@ TEST(QueryPlan, FilterRemove) {
auto expand = MakeExpand(storage, symbol_table, scan_all.op_, scan_all.sym_, "r", EdgeAtom::Direction::BOTH, {}, "m",
false, memgraph::storage::View::OLD);
auto filter_prop = PROPERTY_LOOKUP(IDENT("n")->MapTo(scan_all.sym_), prop);
auto filter = std::make_shared<Filter>(expand.op_, LESS(filter_prop, LITERAL(43)));
auto filter = std::make_shared<Filter>(expand.op_, std::vector<std::shared_ptr<LogicalOperator>>{},
LESS(filter_prop, LITERAL(43)));
// REMOVE n.prop
auto rem_prop = PROPERTY_LOOKUP(IDENT("n")->MapTo(scan_all.sym_), prop);
auto rem = std::make_shared<plan::RemoveProperty>(filter, prop.second, rem_prop);

View File

@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd.
// Copyright 2023 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@ -292,7 +292,7 @@ TEST(QueryPlan, NodeFilterLabelsAndProperties) {
// node filtering
auto *filter_expr = AND(storage.Create<LabelsTest>(n.node_->identifier_, n.node_->labels_),
EQ(PROPERTY_LOOKUP(n.node_->identifier_, property), LITERAL(42)));
auto node_filter = std::make_shared<Filter>(n.op_, filter_expr);
auto node_filter = std::make_shared<Filter>(n.op_, std::vector<std::shared_ptr<LogicalOperator>>{}, filter_expr);
// make a named expression and a produce
auto output = NEXPR("x", IDENT("n")->MapTo(n.sym_))->MapTo(symbol_table.CreateSymbol("named_expression_1", true));
@ -344,7 +344,7 @@ TEST(QueryPlan, NodeFilterMultipleLabels) {
// node filtering
auto *filter_expr = storage.Create<LabelsTest>(n.node_->identifier_, n.node_->labels_);
auto node_filter = std::make_shared<Filter>(n.op_, filter_expr);
auto node_filter = std::make_shared<Filter>(n.op_, std::vector<std::shared_ptr<LogicalOperator>>{}, filter_expr);
// make a named expression and a produce
auto output = NEXPR("n", IDENT("n")->MapTo(n.sym_))->MapTo(symbol_table.CreateSymbol("named_expression_1", true));
@ -679,7 +679,7 @@ class QueryPlanExpandVariable : public testing::Test {
bool is_reverse = false) {
auto n_from = MakeScanAll(storage, symbol_table, node_from, input_op);
auto filter_op = std::make_shared<Filter>(
n_from.op_,
n_from.op_, std::vector<std::shared_ptr<LogicalOperator>>{},
storage.Create<memgraph::query::LabelsTest>(
n_from.node_->identifier_, std::vector<LabelIx>{storage.GetLabelIx(dba.LabelToName(labels[layer]))}));
@ -1355,7 +1355,7 @@ TEST_F(QueryPlanExpandVariable, ExpandToSameSymbol) {
auto n_from = ScanAllTuple{node, logical_op, symbol};
auto filter_op = std::make_shared<Filter>(
n_from.op_,
n_from.op_, std::vector<std::shared_ptr<LogicalOperator>>{},
storage.Create<memgraph::query::LabelsTest>(
n_from.node_->identifier_, std::vector<LabelIx>{storage.GetLabelIx(dba.LabelToName(labels[layer]))}));
@ -1546,7 +1546,7 @@ TEST_F(QueryPlanExpandVariable, FineGrainedExpandToSameSymbol) {
auto n_from = ScanAllTuple{node, logical_op, symbol};
auto filter_op = std::make_shared<Filter>(
n_from.op_,
n_from.op_, std::vector<std::shared_ptr<LogicalOperator>>{},
storage.Create<memgraph::query::LabelsTest>(
n_from.node_->identifier_, std::vector<LabelIx>{storage.GetLabelIx(dba.LabelToName(labels[layer]))}));
@ -1813,7 +1813,8 @@ class QueryPlanExpandWeightedShortestPath : public testing::Test {
auto n = MakeScanAll(storage, symbol_table, "n", existing_node_input ? existing_node_input->op_ : nullptr);
auto last_op = n.op_;
if (node_id) {
last_op = std::make_shared<Filter>(last_op, EQ(PROPERTY_LOOKUP(n.node_->identifier_, prop), LITERAL(*node_id)));
last_op = std::make_shared<Filter>(last_op, std::vector<std::shared_ptr<LogicalOperator>>{},
EQ(PROPERTY_LOOKUP(n.node_->identifier_, prop), LITERAL(*node_id)));
}
auto ident_e = IDENT("e");
@ -1967,8 +1968,9 @@ TEST_F(QueryPlanExpandWeightedShortestPath, ExistingNode) {
// scan the nodes optionally filtering on property value
auto n0 = MakeScanAll(storage, symbol_table, "n0");
if (preceeding_node_id) {
auto filter = std::make_shared<Filter>(
n0.op_, EQ(PROPERTY_LOOKUP(n0.node_->identifier_, prop), LITERAL(*preceeding_node_id)));
auto filter =
std::make_shared<Filter>(n0.op_, std::vector<std::shared_ptr<LogicalOperator>>{},
EQ(PROPERTY_LOOKUP(n0.node_->identifier_, prop), LITERAL(*preceeding_node_id)));
// inject the filter op into the ScanAllTuple. that way the filter
// op can be passed into the ExpandWShortest function without too
// much refactor
@ -2243,7 +2245,8 @@ class QueryPlanExpandAllShortestPaths : public testing::Test {
auto n = MakeScanAll(storage, symbol_table, "n", existing_node_input ? existing_node_input->op_ : nullptr);
auto last_op = n.op_;
if (node_id) {
last_op = std::make_shared<Filter>(last_op, EQ(PROPERTY_LOOKUP(n.node_->identifier_, prop), LITERAL(*node_id)));
last_op = std::make_shared<Filter>(last_op, std::vector<std::shared_ptr<LogicalOperator>>{},
EQ(PROPERTY_LOOKUP(n.node_->identifier_, prop), LITERAL(*node_id)));
}
auto ident_e = IDENT("e");
@ -2739,7 +2742,7 @@ TEST(QueryPlan, OptionalMatchThenExpandToMissingNode) {
n.node_->labels_.emplace_back(storage.GetLabelIx(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);
auto node_filter = std::make_shared<Filter>(n.op_, std::vector<std::shared_ptr<LogicalOperator>>{}, filter_expr);
auto optional = std::make_shared<plan::Optional>(nullptr, node_filter, std::vector<Symbol>{n.sym_});
// WITH n
auto n_ne = NEXPR("n", IDENT("n")->MapTo(n.sym_));
@ -2868,7 +2871,7 @@ TEST(QueryPlan, EdgeFilter) {
r_m.edge_->edge_types_.push_back(storage.GetEdgeTypeIx(dba.EdgeTypeToName(edge_type)));
std::get<0>(r_m.edge_->properties_)[storage.GetPropertyIx(prop.first)] = LITERAL(42);
auto *filter_expr = EQ(PROPERTY_LOOKUP(r_m.edge_->identifier_, prop), LITERAL(42));
auto edge_filter = std::make_shared<Filter>(r_m.op_, filter_expr);
auto edge_filter = std::make_shared<Filter>(r_m.op_, std::vector<std::shared_ptr<LogicalOperator>>{}, filter_expr);
// make a named expression and a produce
auto output =
@ -2936,7 +2939,7 @@ TEST(QueryPlan, Filter) {
auto n = MakeScanAll(storage, symbol_table, "n");
auto e = PROPERTY_LOOKUP(IDENT("n")->MapTo(n.sym_), property);
auto f = std::make_shared<Filter>(n.op_, e);
auto f = std::make_shared<Filter>(n.op_, std::vector<std::shared_ptr<LogicalOperator>>{}, e);
auto output = NEXPR("x", IDENT("n")->MapTo(n.sym_))->MapTo(symbol_table.CreateSymbol("named_expression_1", true));
auto produce = MakeProduce(f, output);
@ -3441,7 +3444,8 @@ TEST(QueryPlan, ScanAllEqualsScanAllByLabelProperty) {
memgraph::query::DbAccessor dba(&storage_dba);
auto scan_all = MakeScanAll(storage, symbol_table, "n");
auto e = PROPERTY_LOOKUP(IDENT("n")->MapTo(scan_all.sym_), std::make_pair("prop", prop));
auto filter = std::make_shared<Filter>(scan_all.op_, EQ(e, LITERAL(prop_value)));
auto filter = std::make_shared<Filter>(scan_all.op_, std::vector<std::shared_ptr<LogicalOperator>>{},
EQ(e, LITERAL(prop_value)));
auto output =
NEXPR("n", IDENT("n")->MapTo(scan_all.sym_))->MapTo(symbol_table.CreateSymbol("named_expression_1", true));
auto produce = MakeProduce(filter, output);
@ -3455,3 +3459,220 @@ TEST(QueryPlan, ScanAllEqualsScanAllByLabelProperty) {
count_with_index(prop_value2, vertex_count - vertex_prop_count);
count_with_scan_all(prop_value2, vertex_count - vertex_prop_count);
}
class ExistsFixture : public testing::Test {
protected:
memgraph::storage::Storage db;
memgraph::storage::Storage::Accessor storage_dba{db.Access()};
memgraph::query::DbAccessor dba{&storage_dba};
AstStorage storage;
SymbolTable symbol_table;
std::pair<std::string, memgraph::storage::PropertyId> prop = PROPERTY_PAIR("property");
memgraph::query::VertexAccessor v1{dba.InsertVertex()};
memgraph::query::VertexAccessor v2{dba.InsertVertex()};
memgraph::storage::EdgeTypeId edge_type{db.NameToEdgeType("Edge")};
memgraph::query::EdgeAccessor r1{*dba.InsertEdge(&v1, &v2, edge_type)};
memgraph::query::VertexAccessor v3{dba.InsertVertex()};
memgraph::query::VertexAccessor v4{dba.InsertVertex()};
memgraph::storage::EdgeTypeId edge_type_unknown{db.NameToEdgeType("Other")};
memgraph::query::EdgeAccessor r2{*dba.InsertEdge(&v3, &v4, edge_type_unknown)};
void SetUp() override {
// (:l1)-[:Edge]->(:l2), (:l3)-[:Other]->(:l4)
ASSERT_TRUE(v1.AddLabel(dba.NameToLabel("l1")).HasValue());
ASSERT_TRUE(v2.AddLabel(dba.NameToLabel("l2")).HasValue());
ASSERT_TRUE(v3.AddLabel(dba.NameToLabel("l3")).HasValue());
ASSERT_TRUE(v4.AddLabel(dba.NameToLabel("l4")).HasValue());
ASSERT_TRUE(v1.SetProperty(prop.second, memgraph::storage::PropertyValue(1)).HasValue());
ASSERT_TRUE(v2.SetProperty(prop.second, memgraph::storage::PropertyValue(2)).HasValue());
ASSERT_TRUE(v3.SetProperty(prop.second, memgraph::storage::PropertyValue(3)).HasValue());
ASSERT_TRUE(v4.SetProperty(prop.second, memgraph::storage::PropertyValue(4)).HasValue());
ASSERT_TRUE(r1.SetProperty(prop.second, memgraph::storage::PropertyValue(1)).HasValue());
memgraph::license::global_license_checker.EnableTesting();
dba.AdvanceCommand();
}
int TestExists(std::string match_label, EdgeAtom::Direction direction,
std::vector<memgraph::storage::EdgeTypeId> edge_types,
std::optional<std::string> destination_label = std::nullopt,
std::optional<int64_t> destination_prop = std::nullopt,
std::optional<int64_t> edge_prop = std::nullopt) {
std::vector<std::string> edge_type_names;
for (const auto &type : edge_types) {
edge_type_names.emplace_back(db.EdgeTypeToName(type));
}
auto *source_node = NODE("n");
auto source_sym = symbol_table.CreateSymbol("n", true);
source_node->identifier_->MapTo(source_sym);
auto *expansion_edge = EDGE("edge", direction, edge_type_names, false);
auto edge_sym = symbol_table.CreateSymbol("edge", false);
expansion_edge->identifier_->MapTo(edge_sym);
auto *destination_node = NODE("n2", destination_label, false);
auto dest_sym = symbol_table.CreateSymbol("n2", false);
destination_node->identifier_->MapTo(dest_sym);
auto *exists_expression = EXISTS(PATTERN(source_node, expansion_edge, destination_node));
exists_expression->MapTo(symbol_table.CreateAnonymousSymbol());
auto scan_all = MakeScanAll(storage, symbol_table, "n");
scan_all.node_->labels_.emplace_back(storage.GetLabelIx(match_label));
std::shared_ptr<LogicalOperator> last_op = std::make_shared<Expand>(
nullptr, scan_all.sym_, dest_sym, edge_sym, direction, edge_types, false, memgraph::storage::View::OLD);
if (destination_label.has_value() || destination_prop.has_value() || edge_prop.has_value()) {
Expression *filter_expr = nullptr;
if (destination_label.has_value()) {
auto labelIx = storage.GetLabelIx(destination_label.value());
destination_node->labels_.emplace_back(labelIx);
auto label_expr = static_cast<Expression *>(
storage.Create<LabelsTest>(destination_node->identifier_, std::vector<memgraph::query::LabelIx>{labelIx}));
filter_expr = filter_expr ? AND(filter_expr, label_expr) : label_expr;
}
if (destination_prop.has_value()) {
auto prop_expr = static_cast<Expression *>(
EQ(PROPERTY_LOOKUP(destination_node->identifier_, prop), LITERAL(destination_prop.value())));
filter_expr = filter_expr ? AND(filter_expr, prop_expr) : prop_expr;
}
if (edge_prop.has_value()) {
auto prop_expr = static_cast<Expression *>(
EQ(PROPERTY_LOOKUP(expansion_edge->identifier_, prop), LITERAL(edge_prop.value())));
filter_expr = filter_expr ? AND(filter_expr, prop_expr) : prop_expr;
}
last_op =
std::make_shared<Filter>(std::move(last_op), std::vector<std::shared_ptr<LogicalOperator>>{}, filter_expr);
}
last_op = std::make_shared<Limit>(std::move(last_op), storage.Create<PrimitiveLiteral>(1));
last_op = std::make_shared<EvaluatePatternFilter>(std::move(last_op), symbol_table.at(*exists_expression));
auto *total_expression =
AND(storage.Create<LabelsTest>(scan_all.node_->identifier_, scan_all.node_->labels_), exists_expression);
auto filter = std::make_shared<Filter>(scan_all.op_, std::vector<std::shared_ptr<LogicalOperator>>{last_op},
total_expression);
auto output =
NEXPR("n", IDENT("n")->MapTo(scan_all.sym_))->MapTo(symbol_table.CreateSymbol("named_expression_1", true));
auto produce = MakeProduce(filter, output);
auto context = MakeContext(storage, symbol_table, &dba);
return PullAll(*produce, &context);
}
int TestDoubleExists(std::string match_label, EdgeAtom::Direction direction,
std::vector<memgraph::storage::EdgeTypeId> first_edge_type,
std::vector<memgraph::storage::EdgeTypeId> second_edge_type, bool or_flag = false) {
std::vector<std::string> first_edge_type_names;
for (const auto &type : first_edge_type) {
first_edge_type_names.emplace_back(db.EdgeTypeToName(type));
}
std::vector<std::string> second_edge_type_names;
for (const auto &type : second_edge_type) {
second_edge_type_names.emplace_back(db.EdgeTypeToName(type));
}
auto *source_node = NODE("n");
auto source_sym = symbol_table.CreateSymbol("n", true);
source_node->identifier_->MapTo(source_sym);
auto *expansion_edge = EDGE("edge", direction, first_edge_type_names, false);
auto edge_sym = symbol_table.CreateSymbol("edge", false);
expansion_edge->identifier_->MapTo(edge_sym);
auto *destination_node = NODE("n2", std::nullopt, false);
auto dest_sym = symbol_table.CreateSymbol("n2", false);
destination_node->identifier_->MapTo(dest_sym);
auto *exists_expression = EXISTS(PATTERN(source_node, expansion_edge, destination_node));
exists_expression->MapTo(symbol_table.CreateAnonymousSymbol());
auto *expansion_edge2 = EDGE("edge2", direction, second_edge_type_names, false);
auto edge_sym2 = symbol_table.CreateSymbol("edge2", false);
expansion_edge2->identifier_->MapTo(edge_sym2);
auto *destination_node2 = NODE("n22", std::nullopt, false);
auto dest_sym2 = symbol_table.CreateSymbol("n22", false);
destination_node2->identifier_->MapTo(dest_sym2);
auto *exists_expression2 = EXISTS(PATTERN(source_node, expansion_edge2, destination_node2));
exists_expression2->MapTo(symbol_table.CreateAnonymousSymbol());
auto scan_all = MakeScanAll(storage, symbol_table, "n");
scan_all.node_->labels_.emplace_back(storage.GetLabelIx(match_label));
std::shared_ptr<LogicalOperator> last_op = std::make_shared<Expand>(
nullptr, scan_all.sym_, dest_sym, edge_sym, direction, first_edge_type, false, memgraph::storage::View::OLD);
last_op = std::make_shared<Limit>(std::move(last_op), storage.Create<PrimitiveLiteral>(1));
last_op = std::make_shared<EvaluatePatternFilter>(std::move(last_op), symbol_table.at(*exists_expression));
std::shared_ptr<LogicalOperator> last_op2 = std::make_shared<Expand>(
nullptr, scan_all.sym_, dest_sym2, edge_sym2, direction, second_edge_type, false, memgraph::storage::View::OLD);
last_op2 = std::make_shared<Limit>(std::move(last_op2), storage.Create<PrimitiveLiteral>(1));
last_op2 = std::make_shared<EvaluatePatternFilter>(std::move(last_op2), symbol_table.at(*exists_expression2));
Expression *total_expression = storage.Create<LabelsTest>(scan_all.node_->identifier_, scan_all.node_->labels_);
if (or_flag) {
total_expression = AND(total_expression, OR(exists_expression, exists_expression2));
} else {
total_expression = AND(total_expression, AND(exists_expression, exists_expression2));
}
auto filter = std::make_shared<Filter>(
scan_all.op_, std::vector<std::shared_ptr<LogicalOperator>>{last_op, last_op2}, total_expression);
auto output =
NEXPR("n", IDENT("n")->MapTo(scan_all.sym_))->MapTo(symbol_table.CreateSymbol("named_expression_1", true));
auto produce = MakeProduce(filter, output);
auto context = MakeContext(storage, symbol_table, &dba);
return PullAll(*produce, &context);
}
};
TEST_F(ExistsFixture, BasicExists) {
std::vector<memgraph::storage::EdgeTypeId> known_edge_types;
known_edge_types.push_back(edge_type);
std::vector<memgraph::storage::EdgeTypeId> unknown_edge_types;
unknown_edge_types.push_back(edge_type_unknown);
EXPECT_EQ(1, TestExists("l1", EdgeAtom::Direction::OUT, {}));
EXPECT_EQ(1, TestExists("l1", EdgeAtom::Direction::BOTH, {}));
EXPECT_EQ(0, TestExists("l1", EdgeAtom::Direction::IN, {}));
EXPECT_EQ(1, TestExists("l1", EdgeAtom::Direction::OUT, known_edge_types));
EXPECT_EQ(0, TestExists("l1", EdgeAtom::Direction::OUT, unknown_edge_types));
}
TEST_F(ExistsFixture, ExistsWithFiltering) {
EXPECT_EQ(1, TestExists("l1", EdgeAtom::Direction::BOTH, {}, "l2"));
EXPECT_EQ(0, TestExists("l1", EdgeAtom::Direction::BOTH, {}, "l3"));
EXPECT_EQ(1, TestExists("l1", EdgeAtom::Direction::BOTH, {}, "l2", 2));
EXPECT_EQ(0, TestExists("l1", EdgeAtom::Direction::BOTH, {}, "l2", 1));
EXPECT_EQ(1, TestExists("l1", EdgeAtom::Direction::BOTH, {}, "l2", std::nullopt, 1));
EXPECT_EQ(0, TestExists("l1", EdgeAtom::Direction::BOTH, {}, "l2", std::nullopt, 2));
EXPECT_EQ(1, TestExists("l1", EdgeAtom::Direction::BOTH, {}, "l2", 2, 1));
EXPECT_EQ(0, TestExists("l1", EdgeAtom::Direction::BOTH, {}, "l2", 1, 1));
}
TEST_F(ExistsFixture, DoubleFilters) {
EXPECT_EQ(1, TestDoubleExists("l1", EdgeAtom::Direction::BOTH, {}, {}, true));
EXPECT_EQ(1, TestDoubleExists("l1", EdgeAtom::Direction::BOTH, {}, {}, false));
}

View File

@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd.
// Copyright 2023 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@ -57,7 +57,8 @@ TEST_F(ReadWriteTypeCheckTest, CreateNode) {
TEST_F(ReadWriteTypeCheckTest, Filter) {
std::shared_ptr<LogicalOperator> scan_all = std::make_shared<ScanAll>(nullptr, GetSymbol("node1"));
std::shared_ptr<LogicalOperator> filter =
std::make_shared<Filter>(scan_all, EQ(PROPERTY_LOOKUP("node1", dba.NameToProperty("prop")), LITERAL(0)));
std::make_shared<Filter>(scan_all, std::vector<std::shared_ptr<LogicalOperator>>{},
EQ(PROPERTY_LOOKUP("node1", dba.NameToProperty("prop")), LITERAL(0)));
CheckPlanType(filter.get(), RWType::R);
}
@ -87,7 +88,8 @@ TEST_F(ReadWriteTypeCheckTest, OrderByAndLimit) {
std::shared_ptr<LogicalOperator> last_op = std::make_shared<Once>();
last_op = std::make_shared<ScanAllByLabel>(last_op, node_sym, label);
last_op = std::make_shared<Filter>(last_op, EQ(PROPERTY_LOOKUP("node", prop), LITERAL(5)));
last_op = std::make_shared<Filter>(last_op, std::vector<std::shared_ptr<LogicalOperator>>{},
EQ(PROPERTY_LOOKUP("node", prop), LITERAL(5)));
last_op = std::make_shared<Produce>(last_op, std::vector<NamedExpression *>{NEXPR("n", IDENT("n"))});
last_op = std::make_shared<OrderBy>(last_op, std::vector<SortItem>{{Ordering::DESC, PROPERTY_LOOKUP("node", prop)}},
std::vector<Symbol>{node_sym});

View File

@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd.
// Copyright 2023 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@ -163,5 +163,4 @@ TEST_F(ExpressionPrettyPrinterTest, NamedExpression) {
// n AS 1
EXPECT_EQ(ToString(NEXPR("n", LITERAL(1))), "(NamedExpression \"n\" 1)");
}
} // namespace

View File

@ -19,6 +19,7 @@
#include "query/frontend/ast/ast.hpp"
#include "query/frontend/semantic/symbol_generator.hpp"
#include "query/frontend/semantic/symbol_table.hpp"
#include "query/plan/preprocess.hpp"
#include "query_common.hpp"
@ -1179,3 +1180,37 @@ TEST_F(TestSymbolGenerator, Foreach) {
query = QUERY(SINGLE_QUERY(FOREACH(i, {CREATE(PATTERN(NODE("n")))}), RETURN("i")));
EXPECT_THROW(memgraph::query::MakeSymbolTable(query), UnboundVariableError);
}
TEST_F(TestSymbolGenerator, Exists) {
auto query = QUERY(SINGLE_QUERY(
MATCH(PATTERN(NODE("n"))),
WHERE(EXISTS(PATTERN(NODE("n"), EDGE("", EdgeAtom::Direction::BOTH, {}, false), NODE("m")))), RETURN("n")));
EXPECT_THROW(MakeSymbolTable(query), SemanticException);
query = QUERY(SINGLE_QUERY(MATCH(PATTERN(NODE("n"))),
WHERE(EXISTS(PATTERN(NODE("n"), EDGE("r"), NODE("", std::nullopt, false)))), RETURN("n")));
EXPECT_THROW(MakeSymbolTable(query), SemanticException);
query = QUERY(
SINGLE_QUERY(MATCH(PATTERN(NODE("n"))), WHERE(EXISTS(PATTERN(NODE("n"), EDGE("r"), NODE("m")))), RETURN("n")));
EXPECT_THROW(MakeSymbolTable(query), SemanticException);
// Symbols for match pattern, node symbol, exists pattern, exists edge, exists second node, named expression in return
query = QUERY(SINGLE_QUERY(MATCH(PATTERN(NODE("n"))),
WHERE(EXISTS(PATTERN(NODE("n"), EDGE("edge", EdgeAtom::Direction::BOTH, {}, false),
NODE("node", std::nullopt, false)))),
RETURN("n")));
auto symbol_table = MakeSymbolTable(query);
ASSERT_EQ(symbol_table.max_position(), 7);
memgraph::query::plan::UsedSymbolsCollector collector(symbol_table);
auto *match = dynamic_cast<Match *>(query->single_query_->clauses_[0]);
auto *expression = dynamic_cast<Expression *>(match->where_->expression_);
expression->Accept(collector);
ASSERT_EQ(collector.symbols_.size(), 1);
auto symbol = *collector.symbols_.begin();
ASSERT_EQ(symbol.name_, "n");
}

View File

@ -6,3 +6,4 @@ python-dateutil==2.6.1
pytz==2017.2
six==1.11.0
tabulate==0.8.1
networkx==2.4