Merge branch 'master' into add-bug-tracking-workflow
This commit is contained in:
commit
d371fefc86
343
include/_mgp_mock.py
Normal file
343
include/_mgp_mock.py
Normal 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
1655
include/mgp_mock.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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*
|
||||
*/
|
||||
|
@ -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_);
|
||||
}
|
||||
|
@ -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 )* )? ')' ;
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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()); }
|
||||
|
@ -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()) {
|
||||
|
@ -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) {}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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_) {
|
||||
|
@ -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.
|
||||
///
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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.") \
|
||||
|
@ -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})
|
||||
|
8
tests/e2e/mock_api/CMakeLists.txt
Normal file
8
tests/e2e/mock_api/CMakeLists.txt
Normal 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)
|
14
tests/e2e/mock_api/common.py
Normal file
14
tests/e2e/mock_api/common.py
Normal 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]
|
10
tests/e2e/mock_api/procedures/CMakeLists.txt
Normal file
10
tests/e2e/mock_api/procedures/CMakeLists.txt
Normal 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)
|
85
tests/e2e/mock_api/procedures/edge.py
Normal file
85
tests/e2e/mock_api/procedures/edge.py
Normal 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)
|
33
tests/e2e/mock_api/procedures/edge_type.py
Normal file
33
tests/e2e/mock_api/procedures/edge_type.py
Normal 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)
|
102
tests/e2e/mock_api/procedures/graph.py
Normal file
102
tests/e2e/mock_api/procedures/graph.py
Normal 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)
|
52
tests/e2e/mock_api/procedures/label.py
Normal file
52
tests/e2e/mock_api/procedures/label.py
Normal 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)
|
65
tests/e2e/mock_api/procedures/path.py
Normal file
65
tests/e2e/mock_api/procedures/path.py
Normal 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)
|
184
tests/e2e/mock_api/procedures/properties.py
Normal file
184
tests/e2e/mock_api/procedures/properties.py
Normal 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)
|
18
tests/e2e/mock_api/procedures/record.py
Normal file
18
tests/e2e/mock_api/procedures/record.py
Normal 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)
|
160
tests/e2e/mock_api/procedures/test_utils.py
Normal file
160
tests/e2e/mock_api/procedures/test_utils.py
Normal 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
|
101
tests/e2e/mock_api/procedures/vertex.py
Normal file
101
tests/e2e/mock_api/procedures/vertex.py
Normal 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)
|
32
tests/e2e/mock_api/procedures/vertices.py
Normal file
32
tests/e2e/mock_api/procedures/vertices.py
Normal 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)
|
195
tests/e2e/mock_api/test_compare_mock.py
Normal file
195
tests/e2e/mock_api/test_compare_mock.py
Normal 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"]))
|
83
tests/e2e/mock_api/workloads.yaml
Normal file
83
tests/e2e/mock_api/workloads.yaml
Normal 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
|
@ -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
|
@ -15,6 +15,7 @@ PIP_DEPS=(
|
||||
"pytest==6.2.3"
|
||||
"pyyaml==5.4.1"
|
||||
"six==1.15.0"
|
||||
"networkx==2.4"
|
||||
)
|
||||
cd "$DIR"
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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");
|
||||
}
|
||||
|
@ -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))
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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);
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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});
|
||||
|
@ -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
|
||||
|
@ -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");
|
||||
}
|
||||
|
@ -6,3 +6,4 @@ python-dateutil==2.6.1
|
||||
pytz==2017.2
|
||||
six==1.11.0
|
||||
tabulate==0.8.1
|
||||
networkx==2.4
|
||||
|
Loading…
Reference in New Issue
Block a user