diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d484dde2..f538bf5e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ### Major Features and Improvements +* CASE construct (without aggregations) + ### Bug Fixes and Other Changes * Keywords appearing in header (named expressions) keep original case. diff --git a/docs/user_technical/open-cypher.md b/docs/user_technical/open-cypher.md index adc33bfcf..a27c936c7 100644 --- a/docs/user_technical/open-cypher.md +++ b/docs/user_technical/open-cypher.md @@ -478,6 +478,10 @@ a dictionary and convert them to strings before running a query: To use parameters with some other driver please consult appropriate documentation. +#### CASE + +TODO + ### Differences Although we try to implement openCypher query language as closely to the diff --git a/src/query/frontend/ast/ast.hpp b/src/query/frontend/ast/ast.hpp index c81b30578..b507061c4 100644 --- a/src/query/frontend/ast/ast.hpp +++ b/src/query/frontend/ast/ast.hpp @@ -447,6 +447,45 @@ class ListSlicingOperator : public Expression { upper_bound_(upper_bound) {} }; +class IfOperator : public Expression { + friend class AstTreeStorage; + + public: + DEFVISITABLE(TreeVisitor<TypedValue>); + bool Accept(HierarchicalTreeVisitor &visitor) override { + if (visitor.PreVisit(*this)) { + condition_->Accept(visitor) && then_expression_->Accept(visitor) && + else_expression_->Accept(visitor); + } + return visitor.PostVisit(*this); + } + + IfOperator *Clone(AstTreeStorage &storage) const override { + return storage.Create<IfOperator>(condition_->Clone(storage), + then_expression_->Clone(storage), + else_expression_->Clone(storage)); + } + + // None of the expressions should be nullptrs. If there is no else_expression + // you probably want to make it NULL PrimitiveLiteral. + Expression *condition_; + Expression *then_expression_; + Expression *else_expression_; + + protected: + IfOperator(int uid, Expression *condition, Expression *then_expression, + Expression *else_expression) + : Expression(uid), + condition_(condition), + then_expression_(then_expression), + else_expression_(else_expression) { + debug_assert( + condition_ != nullptr && then_expression_ != nullptr && + else_expression_ != nullptr, + "clause_, then_expression_ and else_expression_ can't be nullptr"); + } +}; + class NotOperator : public UnaryOperator { friend class AstTreeStorage; diff --git a/src/query/frontend/ast/ast_visitor.hpp b/src/query/frontend/ast/ast_visitor.hpp index 42641b793..e3063ca53 100644 --- a/src/query/frontend/ast/ast_visitor.hpp +++ b/src/query/frontend/ast/ast_visitor.hpp @@ -47,6 +47,7 @@ class GreaterEqualOperator; class InListOperator; class ListIndexingOperator; class ListSlicingOperator; +class IfOperator; class Delete; class Where; class SetProperty; @@ -64,11 +65,11 @@ using TreeCompositeVisitor = ::utils::CompositeVisitor< MultiplicationOperator, DivisionOperator, ModOperator, NotEqualOperator, EqualOperator, LessOperator, GreaterOperator, LessEqualOperator, GreaterEqualOperator, InListOperator, ListIndexingOperator, - ListSlicingOperator, UnaryPlusOperator, UnaryMinusOperator, IsNullOperator, - ListLiteral, MapLiteral, PropertyLookup, LabelsTest, EdgeTypeTest, - Aggregation, Function, All, Create, Match, Return, With, Pattern, NodeAtom, - EdgeAtom, BreadthFirstAtom, Delete, Where, SetProperty, SetProperties, - SetLabels, RemoveProperty, RemoveLabels, Merge, Unwind>; + ListSlicingOperator, IfOperator, UnaryPlusOperator, UnaryMinusOperator, + IsNullOperator, ListLiteral, MapLiteral, PropertyLookup, LabelsTest, + EdgeTypeTest, Aggregation, Function, All, Create, Match, Return, With, + Pattern, NodeAtom, EdgeAtom, BreadthFirstAtom, Delete, Where, SetProperty, + SetProperties, SetLabels, RemoveProperty, RemoveLabels, Merge, Unwind>; using TreeLeafVisitor = ::utils::LeafVisitor<Identifier, PrimitiveLiteral, CreateIndex>; @@ -89,11 +90,11 @@ using TreeVisitor = ::utils::Visitor< MultiplicationOperator, DivisionOperator, ModOperator, NotEqualOperator, EqualOperator, LessOperator, GreaterOperator, LessEqualOperator, GreaterEqualOperator, InListOperator, ListIndexingOperator, - ListSlicingOperator, UnaryPlusOperator, UnaryMinusOperator, IsNullOperator, - ListLiteral, MapLiteral, PropertyLookup, LabelsTest, EdgeTypeTest, - Aggregation, Function, All, Create, Match, Return, With, Pattern, NodeAtom, - EdgeAtom, BreadthFirstAtom, Delete, Where, SetProperty, SetProperties, - SetLabels, RemoveProperty, RemoveLabels, Merge, Unwind, Identifier, - PrimitiveLiteral, CreateIndex>; + ListSlicingOperator, IfOperator, UnaryPlusOperator, UnaryMinusOperator, + IsNullOperator, ListLiteral, MapLiteral, PropertyLookup, LabelsTest, + EdgeTypeTest, Aggregation, Function, All, Create, Match, Return, With, + Pattern, NodeAtom, EdgeAtom, BreadthFirstAtom, 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 2439290a0..05e2f81f4 100644 --- a/src/query/frontend/ast/cypher_main_visitor.cpp +++ b/src/query/frontend/ast/cypher_main_visitor.cpp @@ -797,6 +797,8 @@ 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->caseExpression()) { + return static_cast<Expression *>(ctx->caseExpression()->accept(this)); } // TODO: Implement this. We don't support comprehensions, filtering... at // the moment. @@ -1032,6 +1034,36 @@ antlrcpp::Any CypherMainVisitor::visitPropertyExpression( return dynamic_cast<PropertyLookup *>(expression); } +antlrcpp::Any CypherMainVisitor::visitCaseExpression( + CypherParser::CaseExpressionContext *ctx) { + Expression *test_expression = + ctx->test ? ctx->test->accept(this).as<Expression *>() : nullptr; + auto alternatives = ctx->caseAlternatives(); + // Reverse alternatives so that tree of IfOperators can be built bottom-up. + std::reverse(alternatives.begin(), alternatives.end()); + Expression *else_expression = + ctx->else_expression + ? ctx->else_expression->accept(this).as<Expression *>() + : storage_.Create<PrimitiveLiteral>(TypedValue::Null); + for (auto *alternative : alternatives) { + Expression *condition = + test_expression + ? storage_.Create<EqualOperator>( + test_expression, alternative->when_expression->accept(this)) + : alternative->when_expression->accept(this).as<Expression *>(); + Expression *then_expression = alternative->then_expression->accept(this); + else_expression = storage_.Create<IfOperator>(condition, then_expression, + else_expression); + } + return else_expression; +} + +antlrcpp::Any CypherMainVisitor::visitCaseAlternatives( + CypherParser::CaseAlternativesContext *) { + debug_fail("Should never be called. See documentation in hpp."); + return 0; +} + antlrcpp::Any CypherMainVisitor::visitWith(CypherParser::WithContext *ctx) { auto *with = storage_.Create<With>(); in_with_ = true; diff --git a/src/query/frontend/ast/cypher_main_visitor.hpp b/src/query/frontend/ast/cypher_main_visitor.hpp index 5ad9052c2..c6ca1d50c 100644 --- a/src/query/frontend/ast/cypher_main_visitor.hpp +++ b/src/query/frontend/ast/cypher_main_visitor.hpp @@ -521,6 +521,19 @@ class CypherMainVisitor : public antlropencypher::CypherBaseVisitor { antlrcpp::Any visitPropertyExpression( CypherParser::PropertyExpressionContext *ctx) override; + /** + * @return IfOperator* + */ + antlrcpp::Any visitCaseExpression( + CypherParser::CaseExpressionContext *ctx) override; + + /** + * Never call this. Ast generation for this production is done in + * @c visitCaseExpression. + */ + antlrcpp::Any visitCaseAlternatives( + CypherParser::CaseAlternativesContext *ctx) override; + /** * @return With* */ diff --git a/src/query/frontend/opencypher/grammar/Cypher.g4 b/src/query/frontend/opencypher/grammar/Cypher.g4 index e060e8dc2..192ac6c8e 100644 --- a/src/query/frontend/opencypher/grammar/Cypher.g4 +++ b/src/query/frontend/opencypher/grammar/Cypher.g4 @@ -14,6 +14,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +/* + * When changing this grammar make sure to update constants in + * src/query/frontend/stripped_lexer_constants.hpp (kKeywords, kSpecialTokens + * and bitsets) and src/query/frontend/ast/named_antlr_tokens.hpp if needed. + */ + grammar Cypher; cypher : SP? statement ( SP? ';' )? SP? EOF ; @@ -174,6 +181,7 @@ expression2b : atom ( SP? propertyLookup )* ; atom : literal | parameter + | caseExpression | ( COUNT SP? '(' SP? '*' SP? ')' ) | listComprehension | patternComprehension @@ -232,6 +240,10 @@ patternComprehension : '[' SP? ( variable SP? '=' SP? )? relationshipsPattern SP propertyLookup : '.' SP? ( propertyKeyName ) ; +caseExpression : ( ( CASE ( SP? caseAlternatives )+ ) | ( CASE SP? test=expression ( SP? caseAlternatives )+ ) ) ( SP? ELSE SP? else_expression=expression )? SP? END ; + +caseAlternatives : WHEN SP? when_expression=expression SP? THEN SP? then_expression=expression ; + variable : symbolicName ; StringLiteral : ( '"' ( StringLiteral_0 | EscapedChar )* '"' ) @@ -348,6 +360,11 @@ symbolicName : UnescapedSymbolicName | CONTAINS | IS | CYPHERNULL + | CASE + | WHEN + | THEN + | ELSE + | END | COUNT | FILTER | EXTRACT @@ -429,6 +446,16 @@ IS : ( 'I' | 'i' ) ( 'S' | 's' ) ; CYPHERNULL : ( 'N' | 'n' ) ( 'U' | 'u' ) ( 'L' | 'l' ) ( 'L' | 'l' ) ; +CASE : ( 'C' | 'c' ) ( 'A' | 'a' ) ( 'S' | 's' ) ( 'E' | 'e' ) ; + +ELSE : ( 'E' | 'e' ) ( 'L' | 'l' ) ( 'S' | 's' ) ( 'E' | 'e' ) ; + +END : ( 'E' | 'e' ) ( 'N' | 'n' ) ( 'D' | 'd' ) ; + +WHEN : ( 'W' | 'w' ) ( 'H' | 'h' ) ( 'E' | 'e' ) ( 'N' | 'n' ) ; + +THEN : ( 'T' | 't' ) ( 'H' | 'h' ) ( 'E' | 'e' ) ( 'N' | 'n' ) ; + COUNT : ( 'C' | 'c' ) ( 'O' | 'o' ) ( 'U' | 'u' ) ( 'N' | 'n' ) ( 'T' | 't' ) ; FILTER : ( 'F' | 'f' ) ( 'I' | 'i' ) ( 'L' | 'l' ) ( 'T' | 't' ) ( 'E' | 'e' ) ( 'R' | 'r' ) ; diff --git a/src/query/frontend/semantic/symbol_generator.cpp b/src/query/frontend/semantic/symbol_generator.cpp index 5dc34504f..ad8eb20fd 100644 --- a/src/query/frontend/semantic/symbol_generator.cpp +++ b/src/query/frontend/semantic/symbol_generator.cpp @@ -254,6 +254,17 @@ bool SymbolGenerator::PreVisit(Aggregation &aggr) { "Using aggregation functions inside aggregation functions is not " "allowed"); } + if (scope_.num_if_operators) { + // Neo allows aggregations here and produces very interesting behaviors. + // To simplify implementation at this moment we decided to completely + // disallow aggregations inside of the CASE. + // However, in some cases aggregation makes perfect sense, for example: + // CASE count(n) WHEN 10 THEN "YES" ELSE "NO" END. + // TODO: Rethink of allowing aggregations in some parts of the CASE + // construct. + throw SemanticException( + "Using aggregation functions inside of CASE is not allowed"); + } // Create a virtual symbol for aggregation result. // Currently, we only have aggregation operators which return numbers. symbol_table_[aggr] = @@ -268,6 +279,16 @@ bool SymbolGenerator::PostVisit(Aggregation &) { return true; } +bool SymbolGenerator::PreVisit(IfOperator &) { + ++scope_.num_if_operators; + return true; +} + +bool SymbolGenerator::PostVisit(IfOperator &) { + --scope_.num_if_operators; + return true; +} + bool SymbolGenerator::PreVisit(All &all) { all.list_expression_->Accept(*this); VisitWithIdentifiers(*all.where_, {all.identifier_}); @@ -412,7 +433,7 @@ bool SymbolGenerator::PreVisit(BreadthFirstAtom &bf_atom) { return false; } -bool SymbolGenerator::PostVisit(BreadthFirstAtom &bf_atom) { +bool SymbolGenerator::PostVisit(BreadthFirstAtom &) { scope_.visiting_edge = nullptr; return true; } diff --git a/src/query/frontend/semantic/symbol_generator.hpp b/src/query/frontend/semantic/symbol_generator.hpp index 7b924f3de..d151b77d6 100644 --- a/src/query/frontend/semantic/symbol_generator.hpp +++ b/src/query/frontend/semantic/symbol_generator.hpp @@ -44,6 +44,8 @@ class SymbolGenerator : public HierarchicalTreeVisitor { ReturnType Visit(PrimitiveLiteral &) override { return true; } bool PreVisit(Aggregation &) override; bool PostVisit(Aggregation &) override; + bool PreVisit(IfOperator &) override; + bool PostVisit(IfOperator &) override; bool PreVisit(All &) override; // Pattern and its subparts. @@ -94,6 +96,8 @@ class SymbolGenerator : public HierarchicalTreeVisitor { // Match. Identifiers created by naming vertices, edges and paths are *not* // stored in here. std::vector<Identifier *> identifiers_in_match; + // Number of nested IfOperators. + int num_if_operators{0}; }; bool HasSymbol(const std::string &name); diff --git a/src/query/frontend/semantic/symbol_table.hpp b/src/query/frontend/semantic/symbol_table.hpp index 5b8eef899..84e63242c 100644 --- a/src/query/frontend/semantic/symbol_table.hpp +++ b/src/query/frontend/semantic/symbol_table.hpp @@ -82,4 +82,3 @@ struct hash<query::Symbol> { }; } // namespace std - diff --git a/src/query/frontend/stripped_lexer_constants.hpp b/src/query/frontend/stripped_lexer_constants.hpp index 43b10385c..6751f7468 100644 --- a/src/query/frontend/stripped_lexer_constants.hpp +++ b/src/query/frontend/stripped_lexer_constants.hpp @@ -79,14 +79,14 @@ class Trie { const int kBitsetSize = 65536; const trie::Trie kKeywords = { - "union", "all", "optional", "match", "unwind", "as", - "merge", "on", "create", "set", "detach", "delete", - "remove", "with", "distinct", "return", "order", "by", - "skip", "limit", "ascending", "asc", "descending", "desc", - "where", "or", "xor", "and", "not", "in", - "starts", "ends", "contains", "is", "null", "count", - "filter", "extract", "any", "none", "single", "true", - "false"}; + "union", "all", "optional", "match", "unwind", "as", + "merge", "on", "create", "set", "detach", "delete", + "remove", "with", "distinct", "return", "order", "by", + "skip", "limit", "ascending", "asc", "descending", "desc", + "where", "or", "xor", "and", "not", "in", + "starts", "ends", "contains", "is", "null", "case", + "when", "then", "else", "end", "count", "filter", + "extract", "any", "none", "single", "true", "false"}; // Unicode codepoints that are allowed at the start of the unescaped name. const std::bitset<kBitsetSize> kUnescapedNameAllowedStarts(std::string( diff --git a/src/query/interpret/eval.hpp b/src/query/interpret/eval.hpp index d4de2452d..0c744d07d 100644 --- a/src/query/interpret/eval.hpp +++ b/src/query/interpret/eval.hpp @@ -123,6 +123,22 @@ class ExpressionEvaluator : public TreeVisitor<TypedValue> { return op.expression2_->Accept(*this); } + TypedValue Visit(IfOperator &if_operator) override { + auto condition = if_operator.condition_->Accept(*this); + if (condition.IsNull()) { + return if_operator.then_expression_->Accept(*this); + } + if (condition.type() != TypedValue::Type::Bool) { + // At the moment IfOperator is used only in CASE construct. + throw QueryRuntimeException( + "'CASE' expected boolean expression, but got {}", condition.type()); + } + if (condition.Value<bool>()) { + return if_operator.then_expression_->Accept(*this); + } + return if_operator.else_expression_->Accept(*this); + } + TypedValue Visit(InListOperator &in_list) override { auto literal = in_list.expression1_->Accept(*this); auto _list = in_list.expression2_->Accept(*this); diff --git a/src/query/plan/rule_based_planner.cpp b/src/query/plan/rule_based_planner.cpp index ff2b8f580..6095ca65f 100644 --- a/src/query/plan/rule_based_planner.cpp +++ b/src/query/plan/rule_based_planner.cpp @@ -352,6 +352,23 @@ class ReturnBodyContext : public HierarchicalTreeVisitor { return false; } + bool PreVisit(IfOperator &if_operator) override { + if_operator.condition_->Accept(*this); + bool has_aggr = has_aggregation_.back(); + has_aggregation_.pop_back(); + if_operator.then_expression_->Accept(*this); + has_aggr = has_aggr || has_aggregation_.back(); + has_aggregation_.pop_back(); + if_operator.else_expression_->Accept(*this); + has_aggr = has_aggr || has_aggregation_.back(); + has_aggregation_.pop_back(); + has_aggregation_.emplace_back(has_aggr); + // TODO: Once we allow aggregations here, insert appropriate stuff in + // group_by. + debug_assert(!has_aggr, "Currently aggregations in CASE are not allowed"); + return false; + } + bool PostVisit(Function &function) override { debug_assert(function.arguments_.size() <= has_aggregation_.size(), "Expected has_aggregation_ flags as much as there are " diff --git a/tests/qa/tck_engine/tests/memgraph_V1/features/case.feature b/tests/qa/tck_engine/tests/memgraph_V1/features/case.feature new file mode 100644 index 000000000..eae2b28ed --- /dev/null +++ b/tests/qa/tck_engine/tests/memgraph_V1/features/case.feature @@ -0,0 +1,59 @@ +Feature: Case + + Scenario: Simple CASE: + Given an empty graph + When executing query: + """ + UNWIND range(1, 3) as x RETURN CASE x WHEN 2 THEN "two" END + """ + Then the result should be: + | CASE x WHEN 2 THEN "two" END | + | null | + | 'two' | + | null | + + Scenario: Simple CASE with ELSE: + Given an empty graph + When executing query: + """ + UNWIND range(1, 3) as x RETURN CASE x WHEN 2 THEN "two" ELSE "nottwo" END as z + """ + Then the result should be: + | z | + | 'nottwo' | + | 'two' | + | 'nottwo' | + + Scenario: Generic CASE: + Given an empty graph + When executing query: + """ + UNWIND range(1, 3) as x RETURN CASE WHEN x > 1 THEN "greater" END as z + """ + Then the result should be: + | z | + | null | + | 'greater' | + | 'greater' | + + Scenario: Generic CASE multiple matched whens: + Given an empty graph + When executing query: + """ + UNWIND range(1, 3) as x RETURN CASE WHEN x > 10 THEN 10 WHEN x > 1 THEN 1 WHEN x > 0 THEN 0 WHEN x > "mirko" THEN 1000 END as z + """ + Then the result should be: + | z | + | 0 | + | 1 | + | 1 | + + Scenario: Simple CASE in collect: + Given an empty graph + When executing query: + """ + UNWIND range(1, 3) as x RETURN collect(CASE x WHEN 2 THEN "two" ELSE "nottwo" END) as z + """ + Then the result should be: + | z | + | ['nottwo', 'two', 'nottwo'] | diff --git a/tests/qa/tck_engine/tests/memgraph_V1/features/memgraph.feature b/tests/qa/tck_engine/tests/memgraph_V1/features/memgraph.feature index 4e491c188..bfac9962e 100644 --- a/tests/qa/tck_engine/tests/memgraph_V1/features/memgraph.feature +++ b/tests/qa/tck_engine/tests/memgraph_V1/features/memgraph.feature @@ -62,3 +62,11 @@ Feature: Memgraph only tests (queries in which we choose to be incompatible with CREATE(a:DELete) """ Then an error should be raised + + Scenario: Aggregation in CASE: + Given an empty graph + When executing query: + """ + MATCH (n) RETURN CASE count(n) WHEN 10 THEN 10 END + """ + Then an error should be raised diff --git a/tests/qa/tck_engine/tests/memgraph_V1/features/string_operators.feature b/tests/qa/tck_engine/tests/memgraph_V1/features/string_operators.feature index 28fec96a5..fba0e49ad 100644 --- a/tests/qa/tck_engine/tests/memgraph_V1/features/string_operators.feature +++ b/tests/qa/tck_engine/tests/memgraph_V1/features/string_operators.feature @@ -4,7 +4,7 @@ Feature: String operators Given an empty graph And having executed """ - CREATE(a{name: "ai'M'e"}), (b{name: "AiMe"}), (c{name: "aime"}) + CREATE(a{name: "ai'M'e"}), (b{name: "AiMe"}), (c{name: "aime"}) """ When executing query: """ @@ -20,7 +20,7 @@ Feature: String operators Given an empty graph And having executed """ - CREATE(a{name: "ai'M'e"}), (b{name: "AiMe"}), (c{name: "aime"}) + CREATE(a{name: "ai'M'e"}), (b{name: "AiMe"}), (c{name: "aime"}) """ When executing query: """ @@ -36,7 +36,7 @@ Feature: String operators Given an empty graph And having executed """ - CREATE(a{name: 1}), (b{name: 2}), (c{name: null}) + CREATE(a{name: 1}), (b{name: 2}), (c{name: null}) """ When executing query: """ @@ -50,7 +50,7 @@ Feature: String operators Given an empty graph And having executed """ - CREATE(a{name: 1}), (b{name: 2}), (c{name: null}) + CREATE(a{name: 1}), (b{name: 2}), (c{name: null}) """ When executing query: """ @@ -65,7 +65,7 @@ Feature: String operators Given an empty graph And having executed """ - CREATE(a{name: "ai'M'E"}), (b{name: "AiMe"}), (c{name: "aime"}) + CREATE(a{name: "ai'M'E"}), (b{name: "AiMe"}), (c{name: "aime"}) """ When executing query: """ @@ -82,7 +82,7 @@ Feature: String operators Given an empty graph And having executed """ - CREATE(a{name: "ai'M'e"}), (b{name: "AiMe"}), (c{name: "aime"}) + CREATE(a{name: "ai'M'e"}), (b{name: "AiMe"}), (c{name: "aime"}) """ When executing query: """ @@ -98,7 +98,7 @@ Feature: String operators Given an empty graph And having executed """ - CREATE(a{name: 1}), (b{name: 2}), (c{name: null}) + CREATE(a{name: 1}), (b{name: 2}), (c{name: null}) """ When executing query: """ @@ -112,7 +112,7 @@ Feature: String operators Given an empty graph And having executed """ - CREATE(a{name: 1}), (b{name: 2}), (c{name: null}) + CREATE(a{name: 1}), (b{name: 2}), (c{name: null}) """ When executing query: """ @@ -127,7 +127,7 @@ Feature: String operators Given an empty graph And having executed """ - CREATE(a{name: "ai'M'e"}), (b{name: "AiMe"}), (c{name: "aime"}) + CREATE(a{name: "ai'M'e"}), (b{name: "AiMe"}), (c{name: "aime"}) """ When executing query: """ @@ -143,7 +143,7 @@ Feature: String operators Given an empty graph And having executed """ - CREATE(a{name: "ai'M'e"}), (b{name: "AiMe"}), (c{name: "aime"}) + CREATE(a{name: "ai'M'e"}), (b{name: "AiMe"}), (c{name: "aime"}) """ When executing query: """ @@ -159,7 +159,7 @@ Feature: String operators Given an empty graph And having executed """ - CREATE(a{name: 1}), (b{name: 2}), (c{name: null}) + CREATE(a{name: 1}), (b{name: 2}), (c{name: null}) """ When executing query: """ @@ -169,12 +169,12 @@ Feature: String operators """ Then an error should be raised - + Scenario: Contains test5 Given an empty graph And having executed """ - CREATE(a{name: 1}), (b{name: 2}), (c{name: null}) + CREATE(a{name: 1}), (b{name: 2}), (c{name: null}) """ When executing query: """ @@ -183,4 +183,3 @@ Feature: String operators return n.name """ Then an error should be raised - diff --git a/tests/qa/tck_engine/tests/memgraph_V1/features/unstable.feature b/tests/qa/tck_engine/tests/memgraph_V1/features/unstable.feature index e6c396d0e..6ca3f1dba 100644 --- a/tests/qa/tck_engine/tests/memgraph_V1/features/unstable.feature +++ b/tests/qa/tck_engine/tests/memgraph_V1/features/unstable.feature @@ -59,7 +59,7 @@ Feature: Unstable Given an empty graph And having executed """ - CREATE(a{name: 1}), (b{name: 2}), (c{name: null}) + CREATE(a{name: 1}), (b{name: 2}), (c{name: null}) """ When executing query: """ @@ -73,7 +73,7 @@ Feature: Unstable Given an empty graph And having executed """ - CREATE(a{name: 1}), (b{name: 2}), (c{name: null}) + CREATE(a{name: 1}), (b{name: 2}), (c{name: null}) """ When executing query: """ @@ -87,7 +87,7 @@ Feature: Unstable Given an empty graph And having executed """ - CREATE(a{name: 1}), (b{name: 2}), (c{name: null}) + CREATE(a{name: 1}), (b{name: 2}), (c{name: null}) """ When executing query: """ @@ -105,4 +105,3 @@ Feature: Unstable Then the result should be: | n | | 2.718281828459045 | - diff --git a/tests/unit/cypher_main_visitor.cpp b/tests/unit/cypher_main_visitor.cpp index f8a1db10e..07b521419 100644 --- a/tests/unit/cypher_main_visitor.cpp +++ b/tests/unit/cypher_main_visitor.cpp @@ -541,6 +541,76 @@ TYPED_TEST(CypherMainVisitorTest, InWithListIndexing) { EXPECT_TRUE(list_index); } +TYPED_TEST(CypherMainVisitorTest, CaseGenericForm) { + TypeParam ast_generator( + "RETURN CASE WHEN n < 10 THEN 1 WHEN n > 10 THEN 2 END"); + auto *query = ast_generator.query_; + auto *return_clause = dynamic_cast<Return *>(query->clauses_[0]); + auto *if_operator = dynamic_cast<IfOperator *>( + return_clause->body_.named_expressions[0]->expression_); + ASSERT_TRUE(if_operator); + auto *condition = dynamic_cast<LessOperator *>(if_operator->condition_); + ASSERT_TRUE(condition); + auto *then_expression = + dynamic_cast<PrimitiveLiteral *>(if_operator->then_expression_); + ASSERT_TRUE(then_expression); + ASSERT_EQ(then_expression->value_.Value<int64_t>(), 1); + + auto *if_operator2 = + dynamic_cast<IfOperator *>(if_operator->else_expression_); + ASSERT_TRUE(if_operator2); + auto *condition2 = dynamic_cast<GreaterOperator *>(if_operator2->condition_); + ASSERT_TRUE(condition2); + auto *then_expression2 = + dynamic_cast<PrimitiveLiteral *>(if_operator2->then_expression_); + ASSERT_TRUE(then_expression2); + ASSERT_EQ(then_expression2->value_.Value<int64_t>(), 2); + auto *else_expression2 = + dynamic_cast<PrimitiveLiteral *>(if_operator2->else_expression_); + ASSERT_TRUE(else_expression2); + ASSERT_TRUE(else_expression2->value_.IsNull()); +} + +TYPED_TEST(CypherMainVisitorTest, CaseGenericFormElse) { + TypeParam ast_generator("RETURN CASE WHEN n < 10 THEN 1 ELSE 2 END"); + auto *query = ast_generator.query_; + auto *return_clause = dynamic_cast<Return *>(query->clauses_[0]); + auto *if_operator = dynamic_cast<IfOperator *>( + return_clause->body_.named_expressions[0]->expression_); + auto *condition = dynamic_cast<LessOperator *>(if_operator->condition_); + ASSERT_TRUE(condition); + auto *then_expression = + dynamic_cast<PrimitiveLiteral *>(if_operator->then_expression_); + ASSERT_EQ(then_expression->value_.Value<int64_t>(), 1); + auto *else_expression = + dynamic_cast<PrimitiveLiteral *>(if_operator->else_expression_); + ASSERT_TRUE(else_expression); + ASSERT_EQ(else_expression->value_.Value<int64_t>(), 2); +} + +TYPED_TEST(CypherMainVisitorTest, CaseSimpleForm) { + TypeParam ast_generator("RETURN CASE 5 WHEN 10 THEN 1 END"); + auto *query = ast_generator.query_; + auto *return_clause = dynamic_cast<Return *>(query->clauses_[0]); + auto *if_operator = dynamic_cast<IfOperator *>( + return_clause->body_.named_expressions[0]->expression_); + auto *condition = dynamic_cast<EqualOperator *>(if_operator->condition_); + ASSERT_TRUE(condition); + auto *expr1 = dynamic_cast<PrimitiveLiteral *>(condition->expression1_); + ASSERT_TRUE(expr1); + ASSERT_EQ(expr1->value_.Value<int64_t>(), 5); + auto *expr2 = dynamic_cast<PrimitiveLiteral *>(condition->expression2_); + ASSERT_TRUE(expr2); + ASSERT_EQ(expr2->value_.Value<int64_t>(), 10); + auto *then_expression = + dynamic_cast<PrimitiveLiteral *>(if_operator->then_expression_); + ASSERT_EQ(then_expression->value_.Value<int64_t>(), 1); + auto *else_expression = + dynamic_cast<PrimitiveLiteral *>(if_operator->else_expression_); + ASSERT_TRUE(else_expression); + ASSERT_TRUE(else_expression->value_.IsNull()); +} + TYPED_TEST(CypherMainVisitorTest, IsNull) { TypeParam ast_generator("RETURN 2 iS NulL"); auto *query = ast_generator.query_; diff --git a/tests/unit/query_expression_evaluator.cpp b/tests/unit/query_expression_evaluator.cpp index 9b7a71612..3abb26989 100644 --- a/tests/unit/query_expression_evaluator.cpp +++ b/tests/unit/query_expression_evaluator.cpp @@ -459,6 +459,39 @@ TEST(ExpressionEvaluator, ListSlicingOperator) { } } +TEST(ExpressionEvaluator, IfOperator) { + AstTreeStorage storage; + NoContextExpressionEvaluator eval; + auto *then_expression = storage.Create<PrimitiveLiteral>(10); + auto *else_expression = storage.Create<PrimitiveLiteral>(20); + { + auto *condition_true = + storage.Create<EqualOperator>(storage.Create<PrimitiveLiteral>(2), + storage.Create<PrimitiveLiteral>(2)); + auto *op = storage.Create<IfOperator>(condition_true, then_expression, + else_expression); + auto value = op->Accept(eval.eval); + ASSERT_EQ(value.Value<int64_t>(), 10); + } + { + auto *condition_false = + storage.Create<EqualOperator>(storage.Create<PrimitiveLiteral>(2), + storage.Create<PrimitiveLiteral>(3)); + auto *op = storage.Create<IfOperator>(condition_false, then_expression, + else_expression); + auto value = op->Accept(eval.eval); + ASSERT_EQ(value.Value<int64_t>(), 20); + } + { + auto *condition_exception = + storage.Create<AdditionOperator>(storage.Create<PrimitiveLiteral>(2), + storage.Create<PrimitiveLiteral>(3)); + auto *op = storage.Create<IfOperator>(condition_exception, then_expression, + else_expression); + ASSERT_THROW(op->Accept(eval.eval), QueryRuntimeException); + } +} + TEST(ExpressionEvaluator, NotOperator) { AstTreeStorage storage; NoContextExpressionEvaluator eval;