[master < E214] WHERE Exists feature (#818)

Add WHERE exists() to filter based on neighbouring pattern expressions
This commit is contained in:
Josipmrden 2023-03-07 00:28:41 +01:00 committed by GitHub
parent 99a6c72bba
commit 6abd356d01
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 1891 additions and 261 deletions

View File

@ -2711,5 +2711,41 @@ cpp<#
(:serialize (:slk)) (:serialize (:slk))
(:clone)) (: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 query
(lcp:pop-namespace) ;; namespace memgraph (lcp:pop-namespace) ;; namespace memgraph

View File

@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd. // Copyright 2023 Memgraph Ltd.
// //
// Use of this software is governed by the Business Source License // 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 // 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 VersionQuery;
class Foreach; class Foreach;
class ShowConfigQuery; class ShowConfigQuery;
class Exists;
using TreeCompositeVisitor = utils::CompositeVisitor< using TreeCompositeVisitor = utils::CompositeVisitor<
SingleQuery, CypherUnion, NamedExpression, OrOperator, XorOperator, AndOperator, NotOperator, AdditionOperator, SingleQuery, CypherUnion, NamedExpression, OrOperator, XorOperator, AndOperator, NotOperator, AdditionOperator,
@ -103,7 +104,7 @@ using TreeCompositeVisitor = utils::CompositeVisitor<
ListSlicingOperator, IfOperator, UnaryPlusOperator, UnaryMinusOperator, IsNullOperator, ListLiteral, MapLiteral, ListSlicingOperator, IfOperator, UnaryPlusOperator, UnaryMinusOperator, IsNullOperator, ListLiteral, MapLiteral,
PropertyLookup, LabelsTest, Aggregation, Function, Reduce, Coalesce, Extract, All, Single, Any, None, CallProcedure, PropertyLookup, LabelsTest, Aggregation, Function, Reduce, Coalesce, Extract, All, Single, Any, None, CallProcedure,
Create, Match, Return, With, Pattern, NodeAtom, EdgeAtom, Delete, Where, SetProperty, SetProperties, SetLabels, 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>; using TreeLeafVisitor = utils::LeafVisitor<Identifier, PrimitiveLiteral, ParameterLookup>;
@ -123,7 +124,7 @@ class ExpressionVisitor
LessOperator, GreaterOperator, LessEqualOperator, GreaterEqualOperator, InListOperator, SubscriptOperator, LessOperator, GreaterOperator, LessEqualOperator, GreaterEqualOperator, InListOperator, SubscriptOperator,
ListSlicingOperator, IfOperator, UnaryPlusOperator, UnaryMinusOperator, IsNullOperator, ListLiteral, ListSlicingOperator, IfOperator, UnaryPlusOperator, UnaryMinusOperator, IsNullOperator, ListLiteral,
MapLiteral, PropertyLookup, LabelsTest, Aggregation, Function, Reduce, Coalesce, Extract, All, Single, Any, 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> template <class TResult>
class QueryVisitor : public utils::Visitor<TResult, CypherQuery, ExplainQuery, ProfileQuery, IndexQuery, AuthQuery, class QueryVisitor : public utils::Visitor<TResult, CypherQuery, ExplainQuery, ProfileQuery, IndexQuery, AuthQuery,

View File

@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd. // Copyright 2023 Memgraph Ltd.
// //
// Use of this software is governed by the Business Source License // 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 // 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 *list = std::any_cast<Expression *>(ctx->extractExpression()->idInColl()->expression()->accept(this));
auto *expr = std::any_cast<Expression *>(ctx->extractExpression()->expression()->accept(this)); auto *expr = std::any_cast<Expression *>(ctx->extractExpression()->expression()->accept(this));
return static_cast<Expression *>(storage_->Create<Extract>(ident, list, expr)); 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 // TODO: Implement this. We don't support comprehensions, filtering... at
// the moment. // the moment.
throw utils::NotYetImplemented("atom expression '{}'", ctx->getText()); throw utils::NotYetImplemented("atom expression '{}'", ctx->getText());
@ -2204,6 +2207,17 @@ antlrcpp::Any CypherMainVisitor::visitLiteral(MemgraphCypher::LiteralContext *ct
return visitChildren(ctx); 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) { antlrcpp::Any CypherMainVisitor::visitParenthesizedExpression(MemgraphCypher::ParenthesizedExpressionContext *ctx) {
return std::any_cast<Expression *>(ctx->expression()->accept(this)); return std::any_cast<Expression *>(ctx->expression()->accept(this));
} }

View File

@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd. // Copyright 2023 Memgraph Ltd.
// //
// Use of this software is governed by the Business Source License // 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 // 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; antlrcpp::Any visitParameter(MemgraphCypher::ParameterContext *ctx) override;
/**
* @return Exists* (Expression)
*/
antlrcpp::Any visitExistsExpression(MemgraphCypher::ExistsExpressionContext *ctx) override;
/** /**
* @return Expression* * @return Expression*
*/ */

View File

@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd. // Copyright 2023 Memgraph Ltd.
// //
// Use of this software is governed by the Business Source License // 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 // 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(Reduce &op) override;
void Visit(Coalesce &op) override; void Visit(Coalesce &op) override;
void Visit(Extract &op) override; void Visit(Extract &op) override;
void Visit(Exists &op) override;
void Visit(All &op) override; void Visit(All &op) override;
void Visit(Single &op) override; void Visit(Single &op) override;
void Visit(Any &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_); PrintOperator(out_, "Extract", op.identifier_, op.list_, op.expression_);
} }
void ExpressionPrettyPrinter::Visit(Exists & /*op*/) { PrintOperator(out_, "Exists", "expression"); }
void ExpressionPrettyPrinter::Visit(All &op) { void ExpressionPrettyPrinter::Visit(All &op) {
PrintOperator(out_, "All", op.identifier_, op.list_expression_, op.where_->expression_); PrintOperator(out_, "All", op.identifier_, op.list_expression_, op.where_->expression_);
} }

View File

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

View File

@ -64,6 +64,11 @@ auto SymbolGenerator::CreateSymbol(const std::string &name, bool user_declared,
return symbol; 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) { auto SymbolGenerator::GetOrCreateSymbol(const std::string &name, bool user_declared, Symbol::Type type) {
// NOLINTNEXTLINE // NOLINTNEXTLINE
for (auto scope = scopes_.rbegin(); scope != scopes_.rend(); ++scope) { 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) { if (scope.in_skip || scope.in_limit) {
throw SemanticException("Variables are not allowed in {}.", scope.in_skip ? "SKIP" : "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; Symbol symbol;
if (scope.in_pattern && !(scope.in_node_atom || scope.visiting_edge)) { 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 // 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); symbol = GetOrCreateSymbol(ident.name_, ident.user_declared_, type);
} else if (scope.in_pattern && !scope.in_pattern_atom_identifier && scope.in_match) { } 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 // Prevent variable path bounds to reference the identifier which is bound
// by the variable path itself. // by the variable path itself.
throw UnboundVariableError(ident.name_); throw UnboundVariableError(ident.name_);
@ -430,6 +444,46 @@ bool SymbolGenerator::PreVisit(Extract &extract) {
return false; 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. // Pattern and its subparts.
bool SymbolGenerator::PreVisit(Pattern &pattern) { 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"); MG_ASSERT(utils::IsSubtype(*pattern.atoms_[0], NodeAtom::kType), "Expected a single NodeAtom in Pattern");
scope.in_create_node = true; scope.in_create_node = true;
} }
return true; return true;
} }

View File

@ -64,6 +64,8 @@ class SymbolGenerator : public HierarchicalTreeVisitor {
bool PostVisit(Match &) override; bool PostVisit(Match &) override;
bool PreVisit(Foreach &) override; bool PreVisit(Foreach &) override;
bool PostVisit(Foreach &) override; bool PostVisit(Foreach &) override;
bool PreVisit(SetProperty & /*set_property*/) override;
bool PostVisit(SetProperty & /*set_property*/) override;
// Expressions // Expressions
ReturnType Visit(Identifier &) override; ReturnType Visit(Identifier &) override;
@ -79,6 +81,8 @@ class SymbolGenerator : public HierarchicalTreeVisitor {
bool PreVisit(None &) override; bool PreVisit(None &) override;
bool PreVisit(Reduce &) override; bool PreVisit(Reduce &) override;
bool PreVisit(Extract &) override; bool PreVisit(Extract &) override;
bool PreVisit(Exists & /*exists*/) override;
bool PostVisit(Exists & /*exists*/) override;
// Pattern and its subparts. // Pattern and its subparts.
bool PreVisit(Pattern &) override; bool PreVisit(Pattern &) override;
@ -113,6 +117,8 @@ class SymbolGenerator : public HierarchicalTreeVisitor {
bool in_where{false}; bool in_where{false};
bool in_match{false}; bool in_match{false};
bool in_foreach{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 // True when visiting a pattern atom (node or edge) identifier, which can be
// reused or created in the pattern itself. // reused or created in the pattern itself.
bool in_pattern_atom_identifier{false}; 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, auto CreateSymbol(const std::string &name, bool user_declared, Symbol::Type type = Symbol::Type::ANY,
int token_position = -1); 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); 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 // Returns the symbol by name. If the mapping already exists, checks if the
// types match. Otherwise, returns a new symbol. // types match. Otherwise, returns a new symbol.

View File

@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd. // Copyright 2023 Memgraph Ltd.
// //
// Use of this software is governed by the Business Source License // 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 // 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 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 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 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 // TODO: Remove these since members are public
int32_t max_position() const { return static_cast<int32_t>(table_.size()); } int32_t max_position() const { return static_cast<int32_t>(table_.size()); }

View File

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

View File

@ -116,6 +116,7 @@ extern const Event CartesianOperator;
extern const Event CallProcedureOperator; extern const Event CallProcedureOperator;
extern const Event ForeachOperator; extern const Event ForeachOperator;
extern const Event EmptyResultOperator; extern const Event EmptyResultOperator;
extern const Event EvaluatePatternFilterOperator;
} // namespace EventCounter } // namespace EventCounter
namespace memgraph::query::plan { namespace memgraph::query::plan {
@ -2257,10 +2258,19 @@ std::vector<Symbol> ConstructNamedPath::ModifiedSymbols(const SymbolTable &table
return symbols; return symbols;
} }
Filter::Filter(const std::shared_ptr<LogicalOperator> &input, Expression *expression) Filter::Filter(const std::shared_ptr<LogicalOperator> &input,
: input_(input ? input : std::make_shared<Once>()), expression_(expression) {} 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 { UniqueCursorPtr Filter::MakeCursor(utils::MemoryResource *mem) const {
EventCounter::IncrementCounter(EventCounter::FilterOperator); 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); } 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) 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) { bool Filter::FilterCursor::Pull(Frame &frame, ExecutionContext &context) {
SCOPED_PROFILE_OP("Filter"); 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, ExpressionEvaluator evaluator(&frame, context.symbol_table, context.evaluation_context, context.db_accessor,
storage::View::OLD); storage::View::OLD);
while (input_cursor_->Pull(frame, context)) { 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; if (EvaluateFilter(evaluator, self_.expression_)) return true;
} }
return false; return false;
@ -2290,6 +2320,39 @@ void Filter::FilterCursor::Shutdown() { input_cursor_->Shutdown(); }
void Filter::FilterCursor::Reset() { input_cursor_->Reset(); } 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) 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) {} : input_(input ? input : std::make_shared<Once>()), named_expressions_(named_expressions) {}

View File

@ -133,6 +133,7 @@ class CallProcedure;
class LoadCsv; class LoadCsv;
class Foreach; class Foreach;
class EmptyResult; class EmptyResult;
class EvaluatePatternFilter;
using LogicalOperatorCompositeVisitor = utils::CompositeVisitor< using LogicalOperatorCompositeVisitor = utils::CompositeVisitor<
Once, CreateNode, CreateExpand, ScanAll, ScanAllByLabel, Once, CreateNode, CreateExpand, ScanAll, ScanAllByLabel,
@ -141,7 +142,7 @@ using LogicalOperatorCompositeVisitor = utils::CompositeVisitor<
Expand, ExpandVariable, ConstructNamedPath, Filter, Produce, Delete, Expand, ExpandVariable, ConstructNamedPath, Filter, Produce, Delete,
SetProperty, SetProperties, SetLabels, RemoveProperty, RemoveLabels, SetProperty, SetProperties, SetLabels, RemoveProperty, RemoveLabels,
EdgeUniquenessFilter, Accumulate, Aggregate, Skip, Limit, OrderBy, Merge, 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>; using LogicalOperatorLeafVisitor = utils::LeafVisitor<Once>;
@ -1122,6 +1123,9 @@ pulled.")
((input "std::shared_ptr<LogicalOperator>" :scope :public ((input "std::shared_ptr<LogicalOperator>" :scope :public
:slk-save #'slk-save-operator-pointer :slk-save #'slk-save-operator-pointer
:slk-load #'slk-load-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 (expression "Expression *" :scope :public
:slk-save #'slk-save-ast-pointer :slk-save #'slk-save-ast-pointer
:slk-load (slk-load-ast-pointer "Expression"))) :slk-load (slk-load-ast-pointer "Expression")))
@ -1136,6 +1140,7 @@ a boolean value.")
Filter() {} Filter() {}
Filter(const std::shared_ptr<LogicalOperator> &input_, Filter(const std::shared_ptr<LogicalOperator> &input_,
const std::vector<std::shared_ptr<LogicalOperator>> &pattern_filters_,
Expression *expression_); Expression *expression_);
bool Accept(HierarchicalLogicalOperatorVisitor &visitor) override; bool Accept(HierarchicalLogicalOperatorVisitor &visitor) override;
UniqueCursorPtr MakeCursor(utils::MemoryResource *) const override; UniqueCursorPtr MakeCursor(utils::MemoryResource *) const override;
@ -1159,6 +1164,7 @@ a boolean value.")
private: private:
const Filter &self_; const Filter &self_;
const UniqueCursorPtr input_cursor_; const UniqueCursorPtr input_cursor_;
const std::vector<UniqueCursorPtr> pattern_filter_cursors_;
}; };
cpp<#) cpp<#)
(:serialize (:slk)) (:serialize (:slk))
@ -1777,6 +1783,45 @@ operator's implementation does not expect this.")
(:serialize (:slk)) (:serialize (:slk))
(:clone)) (: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) (lcp:define-class limit (logical-operator)
((input "std::shared_ptr<LogicalOperator>" :scope :public ((input "std::shared_ptr<LogicalOperator>" :scope :public
:slk-save #'slk-save-operator-pointer :slk-save #'slk-save-operator-pointer

View File

@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd. // Copyright 2023 Memgraph Ltd.
// //
// Use of this software is governed by the Business Source License // 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 // 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; 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) { auto SplitExpressionOnAnd(Expression *expression) {
// TODO: Think about converting all filtering expression into CNF to improve // TODO: Think about converting all filtering expression into CNF to improve
// the granularity of filters which can be stand alone. // 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)) { if (!add_prop_is_not_null_check(is_not_null)) {
all_filters_.emplace_back(make_filter(FilterInfo::Type::Generic)); 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 { } else {
all_filters_.emplace_back(make_filter(FilterInfo::Type::Generic)); 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`. // 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, static void ParseForeach(query::Foreach &foreach, SingleQueryPart &query_part, AstStorage &storage,
SymbolTable &symbol_table) { SymbolTable &symbol_table) {
for (auto *clause : foreach.clauses_) { for (auto *clause : foreach.clauses_) {

View File

@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd. // Copyright 2023 Memgraph Ltd.
// //
// Use of this software is governed by the Business Source License // 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 // 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 { 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; return true;
} }
@ -79,6 +98,9 @@ class UsedSymbolsCollector : public HierarchicalTreeVisitor {
std::unordered_set<Symbol> symbols_; std::unordered_set<Symbol> symbols_;
const SymbolTable &symbol_table_; const SymbolTable &symbol_table_;
private:
bool in_exists{false};
}; };
/// Normalized representation of a pattern that needs to be matched. /// Normalized representation of a pattern that needs to be matched.
@ -99,6 +121,93 @@ struct Expansion {
NodeAtom *node2 = nullptr; 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. /// Stores the symbols and expression used to filter a property.
class PropertyFilter { class PropertyFilter {
public: public:
@ -153,7 +262,7 @@ struct FilterInfo {
/// applied for labels or a property. Non generic types contain extra /// applied for labels or a property. Non generic types contain extra
/// information which can be used to produce indexed scans of graph /// information which can be used to produce indexed scans of graph
/// elements. /// elements.
enum class Type { Generic, Label, Property, Id }; enum class Type { Generic, Label, Property, Id, Pattern };
Type type; Type type;
/// The original filter expression which must be satisfied. /// The original filter expression which must be satisfied.
@ -166,6 +275,8 @@ struct FilterInfo {
std::optional<PropertyFilter> property_filter; std::optional<PropertyFilter> property_filter;
/// Information for Type::Id filtering. /// Information for Type::Id filtering.
std::optional<IdFilter> id_filter; 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. /// 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{}; 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 /// @brief Represents a read (+ write) part of a query. Parts are split on
/// `WITH` clauses. /// `WITH` clauses.
/// ///

View File

@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd. // Copyright 2023 Memgraph Ltd.
// //
// Use of this software is governed by the Business Source License // 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 // 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/db_accessor.hpp"
#include "query/frontend/ast/pretty_print.hpp" #include "query/frontend/ast/pretty_print.hpp"
#include "query/plan/operator.hpp"
#include "utils/string.hpp" #include "utils/string.hpp"
namespace memgraph::query::plan { namespace memgraph::query::plan {
@ -148,7 +149,6 @@ bool PlanPrinter::PreVisit(query::plan::Produce &op) {
} }
PRE_VISIT(ConstructNamedPath); PRE_VISIT(ConstructNamedPath);
PRE_VISIT(Filter);
PRE_VISIT(SetProperty); PRE_VISIT(SetProperty);
PRE_VISIT(SetProperties); PRE_VISIT(SetProperties);
PRE_VISIT(SetLabels); PRE_VISIT(SetLabels);
@ -157,6 +157,7 @@ PRE_VISIT(RemoveLabels);
PRE_VISIT(EdgeUniquenessFilter); PRE_VISIT(EdgeUniquenessFilter);
PRE_VISIT(Accumulate); PRE_VISIT(Accumulate);
PRE_VISIT(EmptyResult); PRE_VISIT(EmptyResult);
PRE_VISIT(EvaluatePatternFilter);
bool PlanPrinter::PreVisit(query::plan::Aggregate &op) { bool PlanPrinter::PreVisit(query::plan::Aggregate &op) {
WithPrintLn([&](auto &out) { WithPrintLn([&](auto &out) {
@ -251,6 +252,15 @@ bool PlanPrinter::PreVisit(query::plan::Foreach &op) {
op.input_->Accept(*this); op.input_->Accept(*this);
return false; 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 #undef PRE_VISIT
bool PlanPrinter::DefaultPreVisit() { bool PlanPrinter::DefaultPreVisit() {
@ -589,6 +599,13 @@ bool PlanToJsonVisitor::PreVisit(Filter &op) {
op.input_->Accept(*this); op.input_->Accept(*this);
self["input"] = PopOutput(); 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); output_ = std::move(self);
return false; return false;
} }
@ -908,6 +925,7 @@ bool PlanToJsonVisitor::PreVisit(Cartesian &op) {
output_ = std::move(self); output_ = std::move(self);
return false; return false;
} }
bool PlanToJsonVisitor::PreVisit(Foreach &op) { bool PlanToJsonVisitor::PreVisit(Foreach &op) {
json self; json self;
self["name"] = "Foreach"; self["name"] = "Foreach";
@ -924,6 +942,18 @@ bool PlanToJsonVisitor::PreVisit(Foreach &op) {
return false; 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 impl
} // namespace memgraph::query::plan } // namespace memgraph::query::plan

View File

@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd. // Copyright 2023 Memgraph Ltd.
// //
// Use of this software is governed by the Business Source License // 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 // 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(ConstructNamedPath &) override;
bool PreVisit(Filter &) override; bool PreVisit(Filter &) override;
bool PreVisit(EvaluatePatternFilter & /*unused*/) override;
bool PreVisit(EdgeUniquenessFilter &) override; bool PreVisit(EdgeUniquenessFilter &) override;
bool PreVisit(Merge &) override; bool PreVisit(Merge &) override;
@ -186,6 +187,7 @@ class PlanToJsonVisitor : public virtual HierarchicalLogicalOperatorVisitor {
bool PreVisit(Optional &) override; bool PreVisit(Optional &) override;
bool PreVisit(Filter &) override; bool PreVisit(Filter &) override;
bool PreVisit(EvaluatePatternFilter & /*op*/) override;
bool PreVisit(EdgeUniquenessFilter &) override; bool PreVisit(EdgeUniquenessFilter &) override;
bool PreVisit(Cartesian &) override; bool PreVisit(Cartesian &) override;

View File

@ -454,6 +454,16 @@ class IndexLookupRewriter final : public HierarchicalLogicalOperatorVisitor {
return true; 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_; std::shared_ptr<LogicalOperator> new_root_;
private: private:

View File

@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd. // Copyright 2023 Memgraph Ltd.
// //
// Use of this software is governed by the Business Source License // 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 // 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 { 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. // Ast tree visitor which collects the context for a return body.
// The return body of WITH and RETURN clauses consists of: // 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 // Where may see new symbols so it comes after we generate Produce and in
// general, comes after any OrderBy, Skip or Limit. // general, comes after any OrderBy, Skip or Limit.
if (body.where()) { 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; return last_op;
} }
@ -504,6 +496,12 @@ std::unique_ptr<LogicalOperator> GenReturnBody(std::unique_ptr<LogicalOperator>
namespace impl { 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 *ExtractFilters(const std::unordered_set<Symbol> &bound_symbols, Filters &filters, AstStorage &storage) {
Expression *filter_expr = nullptr; Expression *filter_expr = nullptr;
for (auto filters_it = filters.begin(); filters_it != filters.end();) { 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; 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::unique_ptr<LogicalOperator> GenNamedPaths(std::unique_ptr<LogicalOperator> last_op,
std::unordered_set<Symbol> &bound_symbols, std::unordered_set<Symbol> &bound_symbols,
std::unordered_map<Symbol, std::vector<Symbol>> &named_paths) { std::unordered_map<Symbol, std::vector<Symbol>> &named_paths) {

View File

@ -81,8 +81,8 @@ namespace impl {
// removed from `Filters`. // removed from `Filters`.
Expression *ExtractFilters(const std::unordered_set<Symbol> &, Filters &, AstStorage &); Expression *ExtractFilters(const std::unordered_set<Symbol> &, Filters &, AstStorage &);
std::unique_ptr<LogicalOperator> GenFilters(std::unique_ptr<LogicalOperator>, const std::unordered_set<Symbol> &, /// Checks if the filters has all the bound symbols to be included in the current part of the query
Filters &, AstStorage &); bool HasBoundFilterSymbols(const std::unordered_set<Symbol> &bound_symbols, const FilterInfo &filter);
/// Utility function for iterating pattern atoms and accumulating a result. /// 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 // Try to generate any filters even before the 1st match operator. This
// optimizes the optional match which filters only on symbols bound in // optimizes the optional match which filters only on symbols bound in
// regular match. // regular match.
auto last_op = impl::GenFilters(std::move(input_op), bound_symbols, filters, storage); auto last_op = GenFilters(std::move(input_op), bound_symbols, filters, storage, symbol_table);
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;
if (edge->type_ == EdgeAtom::Type::WEIGHTED_SHORTEST_PATH || last_op = HandleExpansion(std::move(last_op), matching, symbol_table, storage, bound_symbols,
edge->type_ == EdgeAtom::Type::ALL_SHORTEST_PATHS) { match_context.new_symbols, named_paths, filters, match_context.view);
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(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"); MG_ASSERT(named_paths.empty(), "Expected to generate all named paths");
// We bound all named path symbols, so just add them to new_symbols. // We bound all named path symbols, so just add them to new_symbols.
for (const auto &named_path : matching.named_paths) { 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)); 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> HandleForeachClause(query::Foreach *foreach,
std::unique_ptr<LogicalOperator> input_op, std::unique_ptr<LogicalOperator> input_op,
const SymbolTable &symbol_table, 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_, return std::make_unique<plan::Foreach>(std::move(input_op), std::move(op), foreach->named_expression_->expression_,
symbol); 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 } // namespace memgraph::query::plan

View File

@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd. // Copyright 2023 Memgraph Ltd.
// //
// Use of this software is governed by the Business Source License // 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 // 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; 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 } // namespace
VaryMatchingStart::VaryMatchingStart(Matching matching, const SymbolTable &symbol_table) VaryMatchingStart::VaryMatchingStart(Matching matching, const SymbolTable &symbol_table)
@ -209,11 +221,31 @@ CartesianProduct<VaryMatchingStart> VaryMultiMatchingStarts(const std::vector<Ma
return MakeCartesianProduct(std::move(variants)); 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) VaryQueryPartMatching::VaryQueryPartMatching(SingleQueryPart query_part, const SymbolTable &symbol_table)
: query_part_(std::move(query_part)), : query_part_(std::move(query_part)),
matchings_(VaryMatchingStart(query_part_.matching, symbol_table)), matchings_(VaryMatchingStart(query_part_.matching, symbol_table)),
optional_matchings_(VaryMultiMatchingStarts(query_part_.optional_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, VaryQueryPartMatching::iterator::iterator(const SingleQueryPart &query_part,
VaryMatchingStart::iterator matchings_begin, VaryMatchingStart::iterator matchings_begin,
@ -221,7 +253,9 @@ VaryQueryPartMatching::iterator::iterator(const SingleQueryPart &query_part,
CartesianProduct<VaryMatchingStart>::iterator optional_begin, CartesianProduct<VaryMatchingStart>::iterator optional_begin,
CartesianProduct<VaryMatchingStart>::iterator optional_end, CartesianProduct<VaryMatchingStart>::iterator optional_end,
CartesianProduct<VaryMatchingStart>::iterator merge_begin, 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), : current_query_part_(query_part),
matchings_it_(matchings_begin), matchings_it_(matchings_begin),
matchings_end_(matchings_end), matchings_end_(matchings_end),
@ -230,7 +264,10 @@ VaryQueryPartMatching::iterator::iterator(const SingleQueryPart &query_part,
optional_end_(optional_end), optional_end_(optional_end),
merge_it_(merge_begin), merge_it_(merge_begin),
merge_begin_(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_) { if (matchings_it_ != matchings_end_) {
// Fill the query part with the first variation of matchings // Fill the query part with the first variation of matchings
SetCurrentQueryPart(); SetCurrentQueryPart();
@ -242,26 +279,37 @@ VaryQueryPartMatching::iterator &VaryQueryPartMatching::iterator::operator++() {
// * matchings (m1) and (m2) // * matchings (m1) and (m2)
// * optional matchings (o1) and (o2) // * optional matchings (o1) and (o2)
// * merge matching (g1) // * merge matching (g1)
// * filter matching (f1) and (f2)
// We want to produce parts for: // We want to produce parts for:
// * (m1), (o1), (g1) // * (m1), (o1), (g1), (f1)
// * (m1), (o2), (g1) // * (m1), (o1), (g1), (f2)
// * (m2), (o1), (g1) // * (m1), (o2), (g1), (f1)
// * (m2), (o2), (g1) // * (m1), (o2), (g1), (f2)
// Create variations by changing the merge part first. // * (m2), (o1), (g1), (f1)
if (merge_it_ != merge_end_) ++merge_it_; // * (m2), (o1), (g1), (f2)
// If all merge variations are done, start them from beginning and move to the // * (m2), (o2), (g1), (f1)
// next optional matching variation. // * (m2), (o2), (g1), (f2)
if (merge_it_ == merge_end_) {
// 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_; merge_it_ = merge_begin_;
if (optional_it_ != optional_end_) ++optional_it_; 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 if (optional_it_ == optional_end_ && merge_it_ == merge_begin_ && filter_it_ == filter_begin_) {
// matching variation.
if (optional_it_ == optional_end_ && merge_it_ == merge_begin_) {
optional_it_ = optional_begin_; optional_it_ = optional_begin_;
if (matchings_it_ != matchings_end_) ++matchings_it_; if (matchings_it_ != matchings_end_) ++matchings_it_;
} }
// We have reached the end, so return; // We have reached the end, so return;
if (matchings_it_ == matchings_end_) return *this; if (matchings_it_ == matchings_end_) return *this;
// Fill the query part with the new variation of matchings. // Fill the query part with the new variation of matchings.
@ -283,6 +331,28 @@ void VaryQueryPartMatching::iterator::SetCurrentQueryPart() {
if (merge_it_ != merge_end_) { if (merge_it_ != merge_end_) {
current_query_part_.merge_matching = *merge_it_; 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 { 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. // iterators can be at any position.
return true; 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 } // namespace memgraph::query::plan::impl

View File

@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd. // Copyright 2023 Memgraph Ltd.
// //
// Use of this software is governed by the Business Source License // 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 // 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. // Cartesian product of all of them is returned.
CartesianProduct<VaryMatchingStart> VaryMultiMatchingStarts(const std::vector<Matching> &, const SymbolTable &); 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 // Produces alternative query parts out of a single part by varying how each
// graph matching is done. // graph matching is done.
class VaryQueryPartMatching { class VaryQueryPartMatching {
@ -245,6 +247,7 @@ class VaryQueryPartMatching {
typedef const SingleQueryPart *pointer; typedef const SingleQueryPart *pointer;
iterator(const SingleQueryPart &, VaryMatchingStart::iterator, VaryMatchingStart::iterator, 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,
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_it_;
CartesianProduct<VaryMatchingStart>::iterator merge_begin_; CartesianProduct<VaryMatchingStart>::iterator merge_begin_;
CartesianProduct<VaryMatchingStart>::iterator merge_end_; CartesianProduct<VaryMatchingStart>::iterator merge_end_;
CartesianProduct<VaryMatchingStart>::iterator filter_it_;
CartesianProduct<VaryMatchingStart>::iterator filter_begin_;
CartesianProduct<VaryMatchingStart>::iterator filter_end_;
}; };
auto begin() { auto begin() {
return iterator(query_part_, matchings_.begin(), matchings_.end(), optional_matchings_.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() { auto end() {
return iterator(query_part_, matchings_.end(), matchings_.end(), optional_matchings_.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: private:
@ -286,6 +294,7 @@ class VaryQueryPartMatching {
CartesianProduct<VaryMatchingStart> optional_matchings_; CartesianProduct<VaryMatchingStart> optional_matchings_;
// Like optional matching, but for merge matchings. // Like optional matching, but for merge matchings.
CartesianProduct<VaryMatchingStart> merge_matchings_; CartesianProduct<VaryMatchingStart> merge_matchings_;
CartesianProduct<VaryMatchingStart> filter_matchings_;
}; };
} // namespace impl } // namespace impl

View File

@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd. // Copyright 2023 Memgraph Ltd.
// //
// Use of this software is governed by the Business Source License // 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 // 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(CartesianOperator, "Number of times Cartesian operator was used.") \
M(CallProcedureOperator, "Number of times CallProcedure operator was used.") \ M(CallProcedureOperator, "Number of times CallProcedure operator was used.") \
M(ForeachOperator, "Number of times Foreach 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(FailedQuery, "Number of times executing a query failed.") \
M(LabelIndexCreated, "Number of times a label index was created.") \ M(LabelIndexCreated, "Number of times a label index was created.") \

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd. // Copyright 2023 Memgraph Ltd.
// //
// Use of this software is governed by the Business Source License // 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 // 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. /// 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, 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; std::vector<EdgeTypeIx> types;
types.reserve(edge_types.size()); types.reserve(edge_types.size());
for (const auto &type : edge_types) { for (const auto &type : edge_types) {
types.push_back(storage.GetEdgeTypeIx(type)); 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 /// 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. /// Create a NodeAtom with given name and label.
/// ///
/// Name is used to create the Identifier which is assigned to the node. /// 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 GetNode(AstStorage &storage, const std::string &name, std::optional<std::string> label = std::nullopt,
auto node = storage.Create<NodeAtom>(storage.Create<Identifier>(name)); 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)); if (label) node->labels_.emplace_back(storage.GetLabelIx(*label));
return node; 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 COALESCE(...) storage.Create<memgraph::query::Coalesce>(std::vector<memgraph::query::Expression *>{__VA_ARGS__})
#define EXTRACT(variable, list, expr) \ #define EXTRACT(variable, list, expr) \
storage.Create<memgraph::query::Extract>(storage.Create<memgraph::query::Identifier>(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) \ #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), \ storage.Create<memgraph::query::AuthQuery>((action), (user), (role), (user_or_role), password, (privileges), \
(labels), (edgeTypes)) (labels), (edgeTypes))

View File

@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd. // Copyright 2023 Memgraph Ltd.
// //
// Use of this software is governed by the Business Source License // 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 // 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); EXPECT_COST(OP_COST_PARAM + OP_CARD_PARAM * OP_COST_PARAM);
TEST_F(QueryCostEstimator, Filter) { 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) { TEST_F(QueryCostEstimator, EdgeUniquenessFilter) {

View File

@ -1735,4 +1735,119 @@ TYPED_TEST(TestPlanner, Foreach) {
DeleteListContent(&on_create); 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 } // namespace

View File

@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd. // Copyright 2023 Memgraph Ltd.
// //
// Use of this software is governed by the Business Source License // 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 // 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(ScanAllById);
PRE_VISIT(Expand); PRE_VISIT(Expand);
PRE_VISIT(ExpandVariable); PRE_VISIT(ExpandVariable);
PRE_VISIT(Filter);
PRE_VISIT(ConstructNamedPath); PRE_VISIT(ConstructNamedPath);
PRE_VISIT(EmptyResult); PRE_VISIT(EmptyResult);
PRE_VISIT(Produce); PRE_VISIT(Produce);
@ -79,6 +78,7 @@ class PlanChecker : public virtual HierarchicalLogicalOperatorVisitor {
PRE_VISIT(Skip); PRE_VISIT(Skip);
PRE_VISIT(Limit); PRE_VISIT(Limit);
PRE_VISIT(OrderBy); PRE_VISIT(OrderBy);
PRE_VISIT(EvaluatePatternFilter);
bool PreVisit(Merge &op) override { bool PreVisit(Merge &op) override {
CheckOp(op); CheckOp(op);
op.input()->Accept(*this); op.input()->Accept(*this);
@ -97,6 +97,12 @@ class PlanChecker : public virtual HierarchicalLogicalOperatorVisitor {
return false; return false;
} }
bool PreVisit(Filter &op) override {
CheckOp(op);
op.input()->Accept(*this);
return false;
}
bool Visit(Once &) override { bool Visit(Once &) override {
// Ignore checking Once, it is implicitly at the end. // Ignore checking Once, it is implicitly at the end.
return true; return true;
@ -141,7 +147,6 @@ using ExpectScanAll = OpChecker<ScanAll>;
using ExpectScanAllByLabel = OpChecker<ScanAllByLabel>; using ExpectScanAllByLabel = OpChecker<ScanAllByLabel>;
using ExpectScanAllById = OpChecker<ScanAllById>; using ExpectScanAllById = OpChecker<ScanAllById>;
using ExpectExpand = OpChecker<Expand>; using ExpectExpand = OpChecker<Expand>;
using ExpectFilter = OpChecker<Filter>;
using ExpectConstructNamedPath = OpChecker<ConstructNamedPath>; using ExpectConstructNamedPath = OpChecker<ConstructNamedPath>;
using ExpectProduce = OpChecker<Produce>; using ExpectProduce = OpChecker<Produce>;
using ExpectEmptyResult = OpChecker<EmptyResult>; using ExpectEmptyResult = OpChecker<EmptyResult>;
@ -156,6 +161,23 @@ using ExpectLimit = OpChecker<Limit>;
using ExpectOrderBy = OpChecker<OrderBy>; using ExpectOrderBy = OpChecker<OrderBy>;
using ExpectUnwind = OpChecker<Unwind>; using ExpectUnwind = OpChecker<Unwind>;
using ExpectDistinct = OpChecker<Distinct>; 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> { class ExpectForeach : public OpChecker<Foreach> {
public: public:

View File

@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd. // Copyright 2023 Memgraph Ltd.
// //
// Use of this software is governed by the Business Source License // 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 // 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); false, memgraph::storage::View::OLD);
auto *filter_expr = auto *filter_expr =
EQ(storage.Create<PropertyLookup>(scan_all.node_->identifier_, storage.GetPropertyIx(prop.first)), LITERAL(42)); 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 // SET n.prop = n.prop + 1
auto set_prop = PROPERTY_LOOKUP(IDENT("n")->MapTo(scan_all.sym_), prop); auto set_prop = PROPERTY_LOOKUP(IDENT("n")->MapTo(scan_all.sym_), prop);
auto add = ADD(set_prop, LITERAL(1)); 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", auto expand = MakeExpand(storage, symbol_table, scan_all.op_, scan_all.sym_, "r", EdgeAtom::Direction::BOTH, {}, "m",
false, memgraph::storage::View::OLD); false, memgraph::storage::View::OLD);
auto filter_prop = PROPERTY_LOOKUP(IDENT("n")->MapTo(scan_all.sym_), prop); 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 // REMOVE n.prop
auto rem_prop = PROPERTY_LOOKUP(IDENT("n")->MapTo(scan_all.sym_), 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); auto rem = std::make_shared<plan::RemoveProperty>(filter, prop.second, rem_prop);

View File

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

View File

@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd. // Copyright 2023 Memgraph Ltd.
// //
// Use of this software is governed by the Business Source License // 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 // 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) { TEST_F(ReadWriteTypeCheckTest, Filter) {
std::shared_ptr<LogicalOperator> scan_all = std::make_shared<ScanAll>(nullptr, GetSymbol("node1")); std::shared_ptr<LogicalOperator> scan_all = std::make_shared<ScanAll>(nullptr, GetSymbol("node1"));
std::shared_ptr<LogicalOperator> filter = 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); CheckPlanType(filter.get(), RWType::R);
} }
@ -87,7 +88,8 @@ TEST_F(ReadWriteTypeCheckTest, OrderByAndLimit) {
std::shared_ptr<LogicalOperator> last_op = std::make_shared<Once>(); 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<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<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)}}, last_op = std::make_shared<OrderBy>(last_op, std::vector<SortItem>{{Ordering::DESC, PROPERTY_LOOKUP("node", prop)}},
std::vector<Symbol>{node_sym}); std::vector<Symbol>{node_sym});

View File

@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd. // Copyright 2023 Memgraph Ltd.
// //
// Use of this software is governed by the Business Source License // 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 // 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 // n AS 1
EXPECT_EQ(ToString(NEXPR("n", LITERAL(1))), "(NamedExpression \"n\" 1)"); EXPECT_EQ(ToString(NEXPR("n", LITERAL(1))), "(NamedExpression \"n\" 1)");
} }
} // namespace } // namespace

View File

@ -19,6 +19,7 @@
#include "query/frontend/ast/ast.hpp" #include "query/frontend/ast/ast.hpp"
#include "query/frontend/semantic/symbol_generator.hpp" #include "query/frontend/semantic/symbol_generator.hpp"
#include "query/frontend/semantic/symbol_table.hpp" #include "query/frontend/semantic/symbol_table.hpp"
#include "query/plan/preprocess.hpp"
#include "query_common.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"))); query = QUERY(SINGLE_QUERY(FOREACH(i, {CREATE(PATTERN(NODE("n")))}), RETURN("i")));
EXPECT_THROW(memgraph::query::MakeSymbolTable(query), UnboundVariableError); 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");
}