Add SINGLE function to openCypher

Reviewers: florijan, msantl, buda

Reviewed By: msantl

Subscribers: pullbot

Differential Revision: https://phabricator.memgraph.io/D1193
This commit is contained in:
Teon Banek 2018-02-12 16:13:45 +01:00
parent e8608f4050
commit 4326847ab3
15 changed files with 253 additions and 7 deletions

View File

@ -550,6 +550,7 @@ functions.
`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.<br/>The syntax is: `all(variable IN list WHERE predicate)`.<br/> NOTE: Whenever possible, use Memgraph's lambda functions when [matching](#filtering-variable-length-paths) instead.
`single` | Check if only one element of a list satisfies a predicate.<br/>The syntax is: `single(variable IN list WHERE predicate)`.
`reduce` | Accumulate list elements into a single result by applying an expression. The syntax is:<br/>`reduce(accumulator = initial_value, variable IN list | expression)`.
`assert` | Raises an exception reported to the client if the given argument is not `true`.
`counter` | Generates integers that are guaranteed to be unique on the database level, for the given counter name.

View File

@ -78,6 +78,7 @@ BOOST_CLASS_EXPORT_IMPLEMENT(query::Aggregation);
BOOST_CLASS_EXPORT_IMPLEMENT(query::Function);
BOOST_CLASS_EXPORT_IMPLEMENT(query::Reduce);
BOOST_CLASS_EXPORT_IMPLEMENT(query::All);
BOOST_CLASS_EXPORT_IMPLEMENT(query::Single);
BOOST_CLASS_EXPORT_IMPLEMENT(query::ParameterLookup);
BOOST_CLASS_EXPORT_IMPLEMENT(query::Create);
BOOST_CLASS_EXPORT_IMPLEMENT(query::Match);

View File

@ -1523,6 +1523,67 @@ class All : public Expression {
const unsigned int);
};
// TODO: This is pretty much copy pasted from All. Consider merging Reduce, All,
// Any and Single into something like a higher-order function call which takes a
// list argument and a function which is applied on list elements.
class Single : public Expression {
friend class AstTreeStorage;
public:
DEFVISITABLE(TreeVisitor<TypedValue>);
bool Accept(HierarchicalTreeVisitor &visitor) override {
if (visitor.PreVisit(*this)) {
identifier_->Accept(visitor) && list_expression_->Accept(visitor) &&
where_->Accept(visitor);
}
return visitor.PostVisit(*this);
}
Single *Clone(AstTreeStorage &storage) const override {
return storage.Create<Single>(identifier_->Clone(storage),
list_expression_->Clone(storage),
where_->Clone(storage));
}
// None of these should be nullptr after construction.
Identifier *identifier_ = nullptr;
Expression *list_expression_ = nullptr;
Where *where_ = nullptr;
protected:
Single(int uid, Identifier *identifier, Expression *list_expression,
Where *where)
: Expression(uid),
identifier_(identifier),
list_expression_(list_expression),
where_(where) {}
private:
friend class boost::serialization::access;
BOOST_SERIALIZATION_SPLIT_MEMBER();
template <class TArchive>
void save(TArchive &ar, const unsigned int) const {
ar << boost::serialization::base_object<Expression>(*this);
SavePointer(ar, identifier_);
SavePointer(ar, list_expression_);
SavePointer(ar, where_);
}
template <class TArchive>
void load(TArchive &ar, const unsigned int) {
ar >> boost::serialization::base_object<Expression>(*this);
LoadPointer(ar, identifier_);
LoadPointer(ar, list_expression_);
LoadPointer(ar, where_);
}
template <class TArchive>
friend void boost::serialization::load_construct_data(TArchive &, Single *,
const unsigned int);
};
class ParameterLookup : public Expression {
friend class AstTreeStorage;
@ -2962,6 +3023,7 @@ LOAD_AND_CONSTRUCT(query::Aggregation, 0, nullptr, nullptr,
LOAD_AND_CONSTRUCT(query::Reduce, 0, nullptr, nullptr, nullptr, nullptr,
nullptr);
LOAD_AND_CONSTRUCT(query::All, 0, nullptr, nullptr, nullptr);
LOAD_AND_CONSTRUCT(query::Single, 0, nullptr, nullptr, nullptr);
LOAD_AND_CONSTRUCT(query::ParameterLookup, 0);
LOAD_AND_CONSTRUCT(query::NamedExpression, 0);
LOAD_AND_CONSTRUCT(query::NodeAtom, 0);
@ -3022,6 +3084,7 @@ BOOST_CLASS_EXPORT_KEY(query::Aggregation);
BOOST_CLASS_EXPORT_KEY(query::Function);
BOOST_CLASS_EXPORT_KEY(query::Reduce);
BOOST_CLASS_EXPORT_KEY(query::All);
BOOST_CLASS_EXPORT_KEY(query::Single);
BOOST_CLASS_EXPORT_KEY(query::ParameterLookup);
BOOST_CLASS_EXPORT_KEY(query::Create);
BOOST_CLASS_EXPORT_KEY(query::Match);

View File

@ -16,6 +16,7 @@ class Aggregation;
class Function;
class Reduce;
class All;
class Single;
class ParameterLookup;
class Create;
class Match;
@ -68,9 +69,9 @@ using TreeCompositeVisitor = ::utils::CompositeVisitor<
GreaterEqualOperator, InListOperator, ListMapIndexingOperator,
ListSlicingOperator, IfOperator, UnaryPlusOperator, UnaryMinusOperator,
IsNullOperator, ListLiteral, MapLiteral, PropertyLookup, LabelsTest,
Aggregation, Function, Reduce, All, Create, Match, Return, With, Pattern,
NodeAtom, EdgeAtom, Delete, Where, SetProperty, SetProperties, SetLabels,
RemoveProperty, RemoveLabels, Merge, Unwind>;
Aggregation, Function, Reduce, All, Single, Create, Match, Return, With,
Pattern, NodeAtom, EdgeAtom, Delete, Where, SetProperty, SetProperties,
SetLabels, RemoveProperty, RemoveLabels, Merge, Unwind>;
using TreeLeafVisitor = ::utils::LeafVisitor<Identifier, PrimitiveLiteral,
ParameterLookup, CreateIndex>;
@ -93,8 +94,8 @@ using TreeVisitor = ::utils::Visitor<
LessEqualOperator, GreaterEqualOperator, InListOperator,
ListMapIndexingOperator, ListSlicingOperator, IfOperator, UnaryPlusOperator,
UnaryMinusOperator, IsNullOperator, ListLiteral, MapLiteral, PropertyLookup,
LabelsTest, Aggregation, Function, Reduce, All, ParameterLookup, Create,
Match, Return, With, Pattern, NodeAtom, EdgeAtom, Delete, Where,
LabelsTest, Aggregation, Function, Reduce, All, Single, ParameterLookup,
Create, Match, Return, With, Pattern, NodeAtom, EdgeAtom, Delete, Where,
SetProperty, SetProperties, SetLabels, RemoveProperty, RemoveLabels, Merge,
Unwind, Identifier, PrimitiveLiteral, CreateIndex>;

View File

@ -901,6 +901,17 @@ antlrcpp::Any CypherMainVisitor::visitAtom(CypherParser::AtomContext *ctx) {
Where *where = ctx->filterExpression()->where()->accept(this);
return static_cast<Expression *>(
storage_.Create<All>(ident, list_expr, where));
} else if (ctx->SINGLE()) {
auto *ident = storage_.Create<Identifier>(ctx->filterExpression()
->idInColl()
->variable()
->accept(this)
.as<std::string>());
Expression *list_expr =
ctx->filterExpression()->idInColl()->expression()->accept(this);
Where *where = ctx->filterExpression()->where()->accept(this);
return static_cast<Expression *>(
storage_.Create<Single>(ident, list_expr, where));
} else if (ctx->REDUCE()) {
auto *accumulator = storage_.Create<Identifier>(
ctx->reduceExpression()->accumulator->accept(this).as<std::string>());

View File

@ -333,6 +333,12 @@ bool SymbolGenerator::PreVisit(All &all) {
return false;
}
bool SymbolGenerator::PreVisit(Single &single) {
single.list_expression_->Accept(*this);
VisitWithIdentifiers(*single.where_, {single.identifier_});
return false;
}
bool SymbolGenerator::PreVisit(Reduce &reduce) {
reduce.initializer_->Accept(*this);
reduce.list_->Accept(*this);

View File

@ -57,6 +57,7 @@ class SymbolGenerator : public HierarchicalTreeVisitor {
bool PreVisit(IfOperator &) override;
bool PostVisit(IfOperator &) override;
bool PreVisit(All &) override;
bool PreVisit(Single &) override;
bool PreVisit(Reduce &) override;
// Pattern and its subparts.

View File

@ -402,6 +402,40 @@ class ExpressionEvaluator : public TreeVisitor<TypedValue> {
return true;
}
TypedValue Visit(Single &single) override {
auto list_value = single.list_expression_->Accept(*this);
if (list_value.IsNull()) {
return TypedValue::Null;
}
if (list_value.type() != TypedValue::Type::List) {
throw QueryRuntimeException("'SINGLE' expected a list, but got {}",
list_value.type());
}
const auto &list = list_value.Value<std::vector<TypedValue>>();
const auto &symbol = symbol_table_.at(*single.identifier_);
bool predicate_satisfied = false;
for (const auto &element : list) {
frame_[symbol] = element;
auto result = single.where_->expression_->Accept(*this);
if (!result.IsNull() && result.type() != TypedValue::Type::Bool) {
throw QueryRuntimeException(
"Predicate of 'SINGLE' needs to evaluate to 'Boolean', but it "
"resulted in '{}'",
result.type());
}
if (result.IsNull() || !result.Value<bool>()) {
continue;
}
// Return false if more than one element satisfies the predicate.
if (predicate_satisfied) {
return false;
} else {
predicate_satisfied = true;
}
}
return predicate_satisfied;
}
TypedValue Visit(ParameterLookup &param_lookup) override {
return parameters_.AtTokenPosition(param_lookup.token_position_);
}

View File

@ -30,6 +30,13 @@ class UsedSymbolsCollector : public HierarchicalTreeVisitor {
return true;
}
bool PostVisit(Single &single) override {
// Remove the symbol which is bound by single, because we are only
// interested in free (unbound) symbols.
symbols_.erase(symbol_table_.at(*single.identifier_));
return true;
}
bool PostVisit(Reduce &reduce) override {
// Remove the symbols bound by reduce, because we are only interested
// in free (unbound) symbols.
@ -38,7 +45,6 @@ class UsedSymbolsCollector : public HierarchicalTreeVisitor {
return true;
}
bool Visit(Identifier &ident) override {
symbols_.insert(symbol_table_.at(ident));
return true;

View File

@ -210,6 +210,21 @@ class ReturnBodyContext : public HierarchicalTreeVisitor {
return true;
}
bool PostVisit(Single &single) override {
// Remove the symbol which is bound by single, because we are only
// interested in free (unbound) symbols.
used_symbols_.erase(symbol_table_.at(*single.identifier_));
DCHECK(has_aggregation_.size() >= 3U)
<< "Expected 3 has_aggregation_ flags for SINGLE 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 PostVisit(Reduce &reduce) override {
// Remove the symbols bound by reduce, because we are only interested
// in free (unbound) symbols.
@ -226,7 +241,6 @@ class ReturnBodyContext : public HierarchicalTreeVisitor {
return true;
}
bool Visit(Identifier &ident) override {
const auto &symbol = symbol_table_.at(ident);
if (!utils::Contains(output_symbols_, symbol)) {

View File

@ -678,6 +678,31 @@ Feature: Functions
"""
Then an error should be raised
Scenario: Single test 01:
When executing query:
"""
RETURN single(x IN [1, 2, '3'] WHERE x < 4) AS a
"""
Then the result should be:
| a |
| false |
Scenario: Single test 02:
When executing query:
"""
RETURN single(x IN [1, 2, 3] WHERE x = 1) AS a
"""
Then the result should be:
| a |
| true |
Scenario: Single test 03:
When executing query:
"""
RETURN single(x IN [1, 2, '3'] WHERE x > 2) AS a
"""
Then an error should be raised
Scenario: Reduce test 01:
When executing query:
"""

View File

@ -1563,6 +1563,25 @@ TYPED_TEST(CypherMainVisitorTest, ReturnAll) {
EXPECT_TRUE(eq);
}
TYPED_TEST(CypherMainVisitorTest, ReturnSingle) {
TypeParam ast_generator("RETURN single(x IN [1,2,3] WHERE x = 2)");
auto *query = ast_generator.query_;
ASSERT_TRUE(query->single_query_);
auto *single_query = query->single_query_;
ASSERT_EQ(single_query->clauses_.size(), 1U);
auto *ret = dynamic_cast<Return *>(single_query->clauses_[0]);
ASSERT_TRUE(ret);
ASSERT_EQ(ret->body_.named_expressions.size(), 1U);
auto *single =
dynamic_cast<Single *>(ret->body_.named_expressions[0]->expression_);
ASSERT_TRUE(single);
EXPECT_EQ(single->identifier_->name_, "x");
auto *list_literal = dynamic_cast<ListLiteral *>(single->list_expression_);
EXPECT_TRUE(list_literal);
auto *eq = dynamic_cast<EqualOperator *>(single->where_->expression_);
EXPECT_TRUE(eq);
}
TYPED_TEST(CypherMainVisitorTest, ReturnReduce) {
TypeParam ast_generator("RETURN reduce(sum = 0, x IN [1,2,3] | sum + x)");
auto *query = ast_generator.query_;

View File

@ -600,6 +600,9 @@ auto GetMerge(AstTreeStorage &storage, Pattern *pattern, OnMatch on_match,
#define ALL(variable, list, where) \
storage.Create<query::All>(storage.Create<query::Identifier>(variable), \
list, where)
#define SINGLE(variable, list, where) \
storage.Create<query::Single>(storage.Create<query::Identifier>(variable), \
list, where)
#define REDUCE(accumulator, initializer, variable, list, expr) \
storage.Create<query::Reduce>( \
storage.Create<query::Identifier>(accumulator), initializer, \

View File

@ -1221,6 +1221,44 @@ TEST(ExpressionEvaluator, FunctionAllWhereWrongType) {
EXPECT_THROW(all->Accept(eval.eval), QueryRuntimeException);
}
TEST(ExpressionEvaluator, FunctionSingle) {
AstTreeStorage storage;
auto *ident_x = IDENT("x");
auto *single =
SINGLE("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[*single->identifier_] = x_sym;
eval.symbol_table[*ident_x] = x_sym;
auto value = single->Accept(eval.eval);
ASSERT_EQ(value.type(), TypedValue::Type::Bool);
EXPECT_TRUE(value.Value<bool>());
}
TEST(ExpressionEvaluator, FunctionSingle2) {
AstTreeStorage storage;
auto *ident_x = IDENT("x");
auto *single = SINGLE("x", LIST(LITERAL(1), LITERAL(2)),
WHERE(GREATER(ident_x, LITERAL(0))));
NoContextExpressionEvaluator eval;
const auto x_sym = eval.symbol_table.CreateSymbol("x", true);
eval.symbol_table[*single->identifier_] = x_sym;
eval.symbol_table[*ident_x] = x_sym;
auto value = single->Accept(eval.eval);
ASSERT_EQ(value.type(), TypedValue::Type::Bool);
EXPECT_FALSE(value.Value<bool>());
}
TEST(ExpressionEvaluator, FunctionSingleNullList) {
AstTreeStorage storage;
auto *single = SINGLE("x", LITERAL(TypedValue::Null), WHERE(LITERAL(true)));
NoContextExpressionEvaluator eval;
const auto x_sym = eval.symbol_table.CreateSymbol("x", true);
eval.symbol_table[*single->identifier_] = x_sym;
auto value = single->Accept(eval.eval);
EXPECT_TRUE(value.IsNull());
}
TEST(ExpressionEvaluator, FunctionReduce) {
AstTreeStorage storage;
auto *ident_sum = IDENT("sum");

View File

@ -795,6 +795,29 @@ TEST_F(TestSymbolGenerator, WithReturnAll) {
EXPECT_NE(symbol_table.at(*all->identifier_), symbol_table.at(*ret_as_x));
}
TEST_F(TestSymbolGenerator, WithReturnSingle) {
// Test WITH 42 AS x RETURN single(x IN [x] WHERE x = 2) AS x, x AS y
auto *with_as_x = AS("x");
auto *list_x = IDENT("x");
auto *where_x = IDENT("x");
auto *single = SINGLE("x", LIST(list_x), WHERE(EQ(where_x, LITERAL(2))));
auto *ret_as_x = AS("x");
auto *ret_x = IDENT("x");
auto query = QUERY(SINGLE_QUERY(WITH(LITERAL(42), with_as_x),
RETURN(single, ret_as_x, ret_x, AS("y"))));
query->Accept(symbol_generator);
// Symbols for `WITH .. AS x`, `SINGLE(x ...)`, `SINGLE(...) 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(*single->identifier_));
EXPECT_NE(symbol_table.at(*with_as_x), symbol_table.at(*ret_as_x));
// Check `SINGLE(x ...)` is only equal to `WHERE x = 2`
EXPECT_EQ(symbol_table.at(*single->identifier_), symbol_table.at(*where_x));
EXPECT_NE(symbol_table.at(*single->identifier_), symbol_table.at(*ret_as_x));
}
TEST_F(TestSymbolGenerator, WithReturnReduce) {
// Test WITH 42 AS x RETURN reduce(y = 0, x IN [x] y + x) AS x, x AS y
auto *with_as_x = AS("x");