diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4f0ea4270..81b29bd0f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@
### Major Features and Improvements
+* Support for `all` function in openCypher.
* User specified transaction execution timeout.
* Support for query parameters (except for parameters in place of property maps).
diff --git a/docs/user_technical/open-cypher.md b/docs/user_technical/open-cypher.md
index 806638dac..c39cb5e5f 100644
--- a/docs/user_technical/open-cypher.md
+++ b/docs/user_technical/open-cypher.md
@@ -421,6 +421,7 @@ functions.
`startsWith` | Check if the first argument starts with the second.
`endsWith` | Check if the first argument ends with the second.
`contains` | Check if the first argument has an element which is equal to the second argument.
+ `all` | Check if all elements of a list satisfy a predicate.
The syntax is: `all(variable IN list WHERE predicate)`.
#### Parameters
diff --git a/src/query/frontend/ast/ast.hpp b/src/query/frontend/ast/ast.hpp
index 8f3ac8c9b..097864d7a 100644
--- a/src/query/frontend/ast/ast.hpp
+++ b/src/query/frontend/ast/ast.hpp
@@ -1014,6 +1014,42 @@ class Where : public Tree {
Where(int uid, Expression *expression) : Tree(uid), expression_(expression) {}
};
+class All : public Expression {
+ friend class AstTreeStorage;
+
+ public:
+ DEFVISITABLE(TreeVisitor);
+ bool Accept(HierarchicalTreeVisitor &visitor) override {
+ if (visitor.PreVisit(*this)) {
+ identifier_->Accept(visitor) && list_expression_->Accept(visitor) &&
+ where_->Accept(visitor);
+ }
+ return visitor.PostVisit(*this);
+ }
+
+ All *Clone(AstTreeStorage &storage) const override {
+ return storage.Create(identifier_->Clone(storage),
+ list_expression_->Clone(storage),
+ where_->Clone(storage));
+ }
+
+ Identifier *identifier_ = nullptr;
+ Expression *list_expression_ = nullptr;
+ Where *where_ = nullptr;
+
+ protected:
+ All(int uid, Identifier *identifier, Expression *list_expression,
+ Where *where)
+ : Expression(uid),
+ identifier_(identifier),
+ list_expression_(list_expression),
+ where_(where) {
+ debug_assert(identifier, "identifier must not be nullptr");
+ debug_assert(list_expression, "list_expression must not be nullptr");
+ debug_assert(where, "where must not be nullptr");
+ }
+};
+
class Match : public Clause {
friend class AstTreeStorage;
diff --git a/src/query/frontend/ast/ast_visitor.hpp b/src/query/frontend/ast/ast_visitor.hpp
index e4d985530..a73db0202 100644
--- a/src/query/frontend/ast/ast_visitor.hpp
+++ b/src/query/frontend/ast/ast_visitor.hpp
@@ -13,6 +13,7 @@ class LabelsTest;
class EdgeTypeTest;
class Aggregation;
class Function;
+class All;
class Create;
class Match;
class Return;
@@ -63,9 +64,9 @@ using TreeCompositeVisitor = ::utils::CompositeVisitor<
GreaterEqualOperator, InListOperator, ListIndexingOperator,
ListSlicingOperator, UnaryPlusOperator, UnaryMinusOperator, IsNullOperator,
ListLiteral, PropertyLookup, LabelsTest, EdgeTypeTest, Aggregation,
- Function, Create, Match, Return, With, Pattern, NodeAtom, EdgeAtom, Delete,
- Where, SetProperty, SetProperties, SetLabels, RemoveProperty, RemoveLabels,
- Merge, Unwind>;
+ Function, All, Create, Match, Return, With, Pattern, NodeAtom, EdgeAtom,
+ Delete, Where, SetProperty, SetProperties, SetLabels, RemoveProperty,
+ RemoveLabels, Merge, Unwind>;
using TreeLeafVisitor =
::utils::LeafVisitor;
@@ -88,8 +89,8 @@ using TreeVisitor = ::utils::Visitor<
GreaterEqualOperator, InListOperator, ListIndexingOperator,
ListSlicingOperator, UnaryPlusOperator, UnaryMinusOperator, IsNullOperator,
ListLiteral, PropertyLookup, LabelsTest, EdgeTypeTest, Aggregation,
- Function, Create, Match, Return, With, Pattern, NodeAtom, EdgeAtom, Delete,
- Where, SetProperty, SetProperties, SetLabels, RemoveProperty, RemoveLabels,
- Merge, Unwind, Identifier, PrimitiveLiteral, CreateIndex>;
+ Function, All, Create, Match, Return, With, Pattern, NodeAtom, EdgeAtom,
+ Delete, Where, SetProperty, SetProperties, SetLabels, RemoveProperty,
+ RemoveLabels, Merge, Unwind, Identifier, PrimitiveLiteral, CreateIndex>;
} // namespace query
diff --git a/src/query/frontend/ast/cypher_main_visitor.cpp b/src/query/frontend/ast/cypher_main_visitor.cpp
index 0937a1b79..5dff49bbd 100644
--- a/src/query/frontend/ast/cypher_main_visitor.cpp
+++ b/src/query/frontend/ast/cypher_main_visitor.cpp
@@ -756,6 +756,17 @@ antlrcpp::Any CypherMainVisitor::visitAtom(CypherParser::AtomContext *ctx) {
// functionInvocation and atom producions in opencypher grammar.
return static_cast(
storage_.Create(nullptr, Aggregation::Op::COUNT));
+ } else if (ctx->ALL()) {
+ auto *ident = storage_.Create(ctx->filterExpression()
+ ->idInColl()
+ ->variable()
+ ->accept(this)
+ .as());
+ Expression *list_expr =
+ ctx->filterExpression()->idInColl()->expression()->accept(this);
+ Where *where = ctx->filterExpression()->where()->accept(this);
+ return static_cast(
+ storage_.Create(ident, list_expr, where));
}
// TODO: Implement this. We don't support comprehensions, filtering... at
// the moment.
@@ -1024,4 +1035,10 @@ antlrcpp::Any CypherMainVisitor::visitUnwind(CypherParser::UnwindContext *ctx) {
return storage_.Create(named_expr);
}
+antlrcpp::Any CypherMainVisitor::visitFilterExpression(
+ CypherParser::FilterExpressionContext *) {
+ debug_fail("Should never be called. See documentation in hpp.");
+ return 0;
+}
+
} // namespace query::frontend
diff --git a/src/query/frontend/ast/cypher_main_visitor.hpp b/src/query/frontend/ast/cypher_main_visitor.hpp
index 855a4d54a..37944aecf 100644
--- a/src/query/frontend/ast/cypher_main_visitor.hpp
+++ b/src/query/frontend/ast/cypher_main_visitor.hpp
@@ -530,6 +530,13 @@ class CypherMainVisitor : public antlropencypher::CypherBaseVisitor {
*/
antlrcpp::Any visitUnwind(CypherParser::UnwindContext *ctx) override;
+ /**
+ * Never call this. Ast generation for these expressions should be done by
+ * explicitly visiting the members of @c FilterExpressionContext.
+ */
+ antlrcpp::Any visitFilterExpression(
+ CypherParser::FilterExpressionContext *) override;
+
public:
Query *query() { return query_; }
AstTreeStorage &storage() { return storage_; }
diff --git a/src/query/frontend/semantic/symbol_generator.cpp b/src/query/frontend/semantic/symbol_generator.cpp
index 0919feb77..5d49fe2c2 100644
--- a/src/query/frontend/semantic/symbol_generator.cpp
+++ b/src/query/frontend/semantic/symbol_generator.cpp
@@ -4,8 +4,11 @@
#include "query/frontend/semantic/symbol_generator.hpp"
+#include
#include
+#include "utils/algorithm.hpp"
+
namespace query {
auto SymbolGenerator::CreateSymbol(const std::string &name, bool user_declared,
@@ -265,6 +268,28 @@ bool SymbolGenerator::PostVisit(Aggregation &) {
return true;
}
+bool SymbolGenerator::PreVisit(All &all) {
+ all.list_expression_->Accept(*this);
+ // Bind the new symbol after visiting the list expression. Keep the old symbol
+ // so it can be restored.
+ std::experimental::optional prev_symbol;
+ auto prev_symbol_it = scope_.symbols.find(all.identifier_->name_);
+ if (prev_symbol_it != scope_.symbols.end()) {
+ prev_symbol = prev_symbol_it->second;
+ }
+ symbol_table_[*all.identifier_] = CreateSymbol(all.identifier_->name_, true);
+ // Visit Where with the new symbol bound.
+ all.where_->Accept(*this);
+ // Restore the old symbol or just remove the newly bound if there was no
+ // symbol before.
+ if (prev_symbol) {
+ scope_.symbols[all.identifier_->name_] = *prev_symbol;
+ } else {
+ scope_.symbols.erase(all.identifier_->name_);
+ }
+ return false;
+}
+
// Pattern and its subparts.
bool SymbolGenerator::PreVisit(Pattern &pattern) {
diff --git a/src/query/frontend/semantic/symbol_generator.hpp b/src/query/frontend/semantic/symbol_generator.hpp
index 843a6bedc..96ec23eee 100644
--- a/src/query/frontend/semantic/symbol_generator.hpp
+++ b/src/query/frontend/semantic/symbol_generator.hpp
@@ -44,6 +44,7 @@ class SymbolGenerator : public HierarchicalTreeVisitor {
ReturnType Visit(PrimitiveLiteral &) override { return true; }
bool PreVisit(Aggregation &) override;
bool PostVisit(Aggregation &) override;
+ bool PreVisit(All &) override;
// Pattern and its subparts.
bool PreVisit(Pattern &) override;
diff --git a/src/query/interpret/eval.hpp b/src/query/interpret/eval.hpp
index b462711e0..25866932a 100644
--- a/src/query/interpret/eval.hpp
+++ b/src/query/interpret/eval.hpp
@@ -326,6 +326,33 @@ class ExpressionEvaluator : public TreeVisitor {
return function.function_(arguments, db_accessor_);
}
+ TypedValue Visit(All &all) override {
+ auto list_value = all.list_expression_->Accept(*this);
+ if (list_value.IsNull()) {
+ return TypedValue::Null;
+ }
+ if (list_value.type() != TypedValue::Type::List) {
+ throw QueryRuntimeException("'ALL' expected a list, but got {}",
+ list_value.type());
+ }
+ const auto &list = list_value.Value>();
+ const auto &symbol = symbol_table_.at(*all.identifier_);
+ for (const auto &element : list) {
+ frame_[symbol] = element;
+ auto result = all.where_->expression_->Accept(*this);
+ if (!result.IsNull() && result.type() != TypedValue::Type::Bool) {
+ throw QueryRuntimeException(
+ "Predicate of 'ALL' needs to evaluate to 'Boolean', but it "
+ "resulted in '{}'",
+ result.type());
+ }
+ if (result.IsNull() || !result.Value()) {
+ return result;
+ }
+ }
+ return true;
+ }
+
private:
// If the given TypedValue contains accessors, switch them to New or Old,
// depending on use_new_ flag.
diff --git a/src/query/plan/rule_based_planner.cpp b/src/query/plan/rule_based_planner.cpp
index 20bfeb902..fed8abf2a 100644
--- a/src/query/plan/rule_based_planner.cpp
+++ b/src/query/plan/rule_based_planner.cpp
@@ -133,16 +133,22 @@ class UsedSymbolsCollector : public HierarchicalTreeVisitor {
using HierarchicalTreeVisitor::PreVisit;
using HierarchicalTreeVisitor::PostVisit;
- using typename HierarchicalTreeVisitor::ReturnType;
using HierarchicalTreeVisitor::Visit;
- ReturnType Visit(Identifier &ident) override {
+ bool PostVisit(All &all) override {
+ // Remove the symbol which is bound by all, because we are only interested
+ // in free (unbound) symbols.
+ symbols_.erase(symbol_table_.at(*all.identifier_));
+ return true;
+ }
+
+ bool Visit(Identifier &ident) override {
symbols_.insert(symbol_table_.at(ident));
return true;
}
- ReturnType Visit(PrimitiveLiteral &) override { return true; }
- ReturnType Visit(query::CreateIndex &) override { return true; }
+ bool Visit(PrimitiveLiteral &) override { return true; }
+ bool Visit(query::CreateIndex &) override { return true; }
std::unordered_set symbols_;
const SymbolTable &symbol_table_;
@@ -280,6 +286,21 @@ class ReturnBodyContext : public HierarchicalTreeVisitor {
return true;
}
+ bool PostVisit(All &all) override {
+ // Remove the symbol which is bound by all, because we are only interested
+ // in free (unbound) symbols.
+ used_symbols_.erase(symbol_table_.at(*all.identifier_));
+ debug_assert(has_aggregation_.size() >= 3U,
+ "Expected 3 has_aggregation_ flags for ALL arguments");
+ bool has_aggr = false;
+ for (int i = 0; i < 3; ++i) {
+ has_aggr = has_aggr || has_aggregation_.back();
+ has_aggregation_.pop_back();
+ }
+ has_aggregation_.emplace_back(has_aggr);
+ return true;
+ }
+
bool Visit(Identifier &ident) override {
const auto &symbol = symbol_table_.at(ident);
if (std::find(output_symbols_.begin(), output_symbols_.end(), symbol) ==
diff --git a/tests/qa/tck_engine/tests/memgraph_V1/features/functions.feature b/tests/qa/tck_engine/tests/memgraph_V1/features/functions.feature
index 0c2112a91..a71cb7829 100644
--- a/tests/qa/tck_engine/tests/memgraph_V1/features/functions.feature
+++ b/tests/qa/tck_engine/tests/memgraph_V1/features/functions.feature
@@ -596,16 +596,6 @@ Feature: Functions
| (:y) | false | true |
| (:y) | false | true |
- # Scenario: Keys test:
- # Given an empty graph
- # When executing query:
- # """
- # CREATE (n{x: 123, a: null, b: 'x', d: 1}) RETURN KEYS(n) AS a
- # """
- # Then the result should be (ignoring element order for lists)
- # | a |
- # | ['x', 'null', 'b'] |
- #
Scenario: Pi test:
When executing query:
"""
@@ -614,3 +604,28 @@ Feature: Functions
Then the result should be:
| n |
| 3.141592653589793 |
+
+ Scenario: All test 01:
+ When executing query:
+ """
+ RETURN all(x IN [1, 2, '3'] WHERE x < 2) AS a
+ """
+ Then the result should be:
+ | a |
+ | false |
+
+ Scenario: All test 02:
+ When executing query:
+ """
+ RETURN all(x IN [1, 2, 3] WHERE x < 4) AS a
+ """
+ Then the result should be:
+ | a |
+ | true |
+
+ Scenario: All test 03:
+ When executing query:
+ """
+ RETURN all(x IN [1, 2, '3'] WHERE x < 3) AS a
+ """
+ Then an error should be raised
diff --git a/tests/unit/cypher_main_visitor.cpp b/tests/unit/cypher_main_visitor.cpp
index 201b7f05e..61e5d5c78 100644
--- a/tests/unit/cypher_main_visitor.cpp
+++ b/tests/unit/cypher_main_visitor.cpp
@@ -1383,4 +1383,21 @@ TYPED_TEST(CypherMainVisitorTest, CreateIndex) {
ASSERT_EQ(create_index->property_,
ast_generator.db_accessor_->property("slavko"));
}
+
+TYPED_TEST(CypherMainVisitorTest, ReturnAll) {
+ TypeParam ast_generator("RETURN all(x IN [1,2,3] WHERE x = 2)");
+ auto *query = ast_generator.query_;
+ ASSERT_EQ(query->clauses_.size(), 1U);
+ auto *ret = dynamic_cast(query->clauses_[0]);
+ ASSERT_TRUE(ret);
+ ASSERT_EQ(ret->body_.named_expressions.size(), 1U);
+ auto *all = dynamic_cast(ret->body_.named_expressions[0]->expression_);
+ ASSERT_TRUE(all);
+ EXPECT_EQ(all->identifier_->name_, "x");
+ auto *list_literal = dynamic_cast(all->list_expression_);
+ EXPECT_TRUE(list_literal);
+ auto *eq = dynamic_cast(all->where_->expression_);
+ EXPECT_TRUE(eq);
+}
+
}
diff --git a/tests/unit/query_common.hpp b/tests/unit/query_common.hpp
index 958bd8254..9a3657b19 100644
--- a/tests/unit/query_common.hpp
+++ b/tests/unit/query_common.hpp
@@ -483,3 +483,7 @@ auto GetMerge(AstTreeStorage &storage, Pattern *pattern, OnMatch on_match,
// List slicing
#define SLICE(list, lower_bound, upper_bound) \
storage.Create(list, lower_bound, upper_bound)
+// all(variable IN list WHERE predicate)
+#define ALL(variable, list, where) \
+ storage.Create(storage.Create(variable), \
+ list, where)
diff --git a/tests/unit/query_expression_evaluator.cpp b/tests/unit/query_expression_evaluator.cpp
index 35136edbb..ac80bcb65 100644
--- a/tests/unit/query_expression_evaluator.cpp
+++ b/tests/unit/query_expression_evaluator.cpp
@@ -6,14 +6,15 @@
#include "gmock/gmock.h"
#include "gtest/gtest.h"
+#include "database/dbms.hpp"
#include "database/graph_db_accessor.hpp"
#include "database/graph_db_datatypes.hpp"
-#include "database/dbms.hpp"
#include "query/frontend/ast/ast.hpp"
#include "query/frontend/opencypher/parser.hpp"
#include "query/interpret/awesome_memgraph_functions.hpp"
#include "query/interpret/eval.hpp"
#include "query/interpret/frame.hpp"
+
#include "query_common.hpp"
using namespace query;
@@ -1007,4 +1008,38 @@ TEST(ExpressionEvaluator, FunctionContains) {
EXPECT_FALSE(EvaluateFunction(kContains, {"cde", "abcdef"}).Value());
EXPECT_FALSE(EvaluateFunction(kContains, {"abcdef", "dEf"}).Value());
}
+
+TEST(ExpressionEvaluator, FunctionAll) {
+ AstTreeStorage storage;
+ auto *ident_x = IDENT("x");
+ auto *all =
+ ALL("x", LIST(LITERAL(1), LITERAL(2)), WHERE(EQ(ident_x, LITERAL(1))));
+ NoContextExpressionEvaluator eval;
+ const auto x_sym = eval.symbol_table.CreateSymbol("x", true);
+ eval.symbol_table[*all->identifier_] = x_sym;
+ eval.symbol_table[*ident_x] = x_sym;
+ auto value = all->Accept(eval.eval);
+ ASSERT_EQ(value.type(), TypedValue::Type::Bool);
+ EXPECT_FALSE(value.Value());
+}
+
+TEST(ExpressionEvaluator, FunctionAllNullList) {
+ AstTreeStorage storage;
+ auto *all = ALL("x", LITERAL(TypedValue::Null), WHERE(LITERAL(true)));
+ NoContextExpressionEvaluator eval;
+ const auto x_sym = eval.symbol_table.CreateSymbol("x", true);
+ eval.symbol_table[*all->identifier_] = x_sym;
+ auto value = all->Accept(eval.eval);
+ EXPECT_TRUE(value.IsNull());
+}
+
+TEST(ExpressionEvaluator, FunctionAllWhereWrongType) {
+ AstTreeStorage storage;
+ auto *all = ALL("x", LIST(LITERAL(1)), WHERE(LITERAL(2)));
+ NoContextExpressionEvaluator eval;
+ const auto x_sym = eval.symbol_table.CreateSymbol("x", true);
+ eval.symbol_table[*all->identifier_] = x_sym;
+ EXPECT_THROW(all->Accept(eval.eval), QueryRuntimeException);
+}
+
}
diff --git a/tests/unit/query_planner.cpp b/tests/unit/query_planner.cpp
index a7001adf5..1046c1a24 100644
--- a/tests/unit/query_planner.cpp
+++ b/tests/unit/query_planner.cpp
@@ -1205,4 +1205,14 @@ TEST(TestLogicalPlanner, UnableToUsePropertyIndex) {
ExpectProduce());
}
+TEST(TestLogicalPlanner, ReturnSumGroupByAll) {
+ // Test RETURN sum([1,2,3]), all(x in [1] where x = 1)
+ AstTreeStorage storage;
+ auto sum = SUM(LIST(LITERAL(1), LITERAL(2), LITERAL(3)));
+ auto *all = ALL("x", LIST(LITERAL(1)), WHERE(EQ(IDENT("x"), LITERAL(1))));
+ QUERY(RETURN(sum, AS("sum"), all, AS("all")));
+ auto aggr = ExpectAggregate({sum}, {all});
+ CheckPlan(storage, aggr, ExpectProduce());
+}
+
} // namespace
diff --git a/tests/unit/query_semantic.cpp b/tests/unit/query_semantic.cpp
index 00e1ae481..06e506220 100644
--- a/tests/unit/query_semantic.cpp
+++ b/tests/unit/query_semantic.cpp
@@ -984,5 +984,30 @@ TEST(TestSymbolGenerator, MatchPropertySameIdentifier) {
EXPECT_EQ(n, symbol_table.at(*n_prop->expression_));
}
+TEST(TestSymbolGenerator, WithReturnAll) {
+ // Test WITH 42 AS x RETURN all(x IN [x] WHERE x = 2) AS x, x AS y
+ AstTreeStorage storage;
+ auto *with_as_x = AS("x");
+ auto *list_x = IDENT("x");
+ auto *where_x = IDENT("x");
+ auto *all = ALL("x", LIST(list_x), WHERE(EQ(where_x, LITERAL(2))));
+ auto *ret_as_x = AS("x");
+ auto *ret_x = IDENT("x");
+ auto query = QUERY(WITH(LITERAL(42), with_as_x),
+ RETURN(all, ret_as_x, ret_x, AS("y")));
+ SymbolTable symbol_table;
+ SymbolGenerator symbol_generator(symbol_table);
+ query->Accept(symbol_generator);
+ // Symbols for `WITH .. AS x`, `ALL(x ...)`, `ALL(...) AS x` and `AS y`.
+ EXPECT_EQ(symbol_table.max_position(), 4);
+ // Check `WITH .. AS x` is the same as `[x]` and `RETURN ... x AS y`
+ EXPECT_EQ(symbol_table.at(*with_as_x), symbol_table.at(*list_x));
+ EXPECT_EQ(symbol_table.at(*with_as_x), symbol_table.at(*ret_x));
+ EXPECT_NE(symbol_table.at(*with_as_x), symbol_table.at(*all->identifier_));
+ EXPECT_NE(symbol_table.at(*with_as_x), symbol_table.at(*ret_as_x));
+ // Check `ALL(x ...)` is only equal to `WHERE x = 2`
+ EXPECT_EQ(symbol_table.at(*all->identifier_), symbol_table.at(*where_x));
+ EXPECT_NE(symbol_table.at(*all->identifier_), symbol_table.at(*ret_as_x));
+}
} // namespace