diff --git a/.ycm_extra_conf.py b/.ycm_extra_conf.py index 8b3b3c85a..3a773a4d1 100644 --- a/.ycm_extra_conf.py +++ b/.ycm_extra_conf.py @@ -21,8 +21,9 @@ BASE_FLAGS = [ '-I./include', '-I./libs/fmt', '-I./libs/yaml-cpp', - '-I./build/googletest-src/googletest/include', - '-I./build/googlebenchmark-src/include', + '-I./libs/googletest/googletest/include', + '-I./libs/googletest/googlemock/include', + '-I./libs/benchmark/include', '-I./libs/antlr4/runtime/Cpp/runtime/src' ] diff --git a/CMakeLists.txt b/CMakeLists.txt index 053559741..49e66bb16 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -332,6 +332,7 @@ set(memgraph_src_files ${src_dir}/logging/log.cpp ${src_dir}/database/graph_db.cpp ${src_dir}/database/graph_db_accessor.cpp + ${src_dir}/query/backend/cpp/cypher_main_visitor.cpp ) # ----------------------------------------------------------------------------- diff --git a/src/query/backend/cpp/compiler_structures.hpp b/src/query/backend/cpp/compiler_structures.hpp new file mode 100644 index 000000000..1c14d80a4 --- /dev/null +++ b/src/query/backend/cpp/compiler_structures.hpp @@ -0,0 +1,52 @@ +#pragma once + +#include <climits> +#include <unordered_map> +#include "query/frontend/opencypher/generated/CypherParser.h" +#include "utils/exceptions/basic_exception.hpp" + +// TODO: Figure out what information to put in exception. +// Error reporting is tricky since we get stripped query and position of error +// in original query is not same as position of error in stripped query. Most +// correct approach would be to do semantic analysis with original query even +// for already hashed queries, but that has obvious performance issues. Other +// approach would be to report some of the semantic errors in runtime of the +// query and only report line numbers of semantic errors (not position in the +// line) if multiple line strings are not allowed by grammar. We could also +// print whole line that contains error instead of specifying line number. +class SemanticException : BasicException { + public: + SemanticException() : BasicException("") {} +}; + +// enum VariableType { TYPED_VALUE, LIST, MAP, NODE, RELATIONSHIP, PATH }; + +struct Node { + std::string output_identifier; + std::vector<std::string> labels; + std::unordered_map<std::string, + antlropencypher::CypherParser::ExpressionContext*> + properties; +}; + +struct Relationship { + enum Direction { LEFT, RIGHT, BOTH }; + std::string output_identifier; + Direction direction = Direction::BOTH; + std::vector<std::string> types; + std::unordered_map<std::string, + antlropencypher::CypherParser::ExpressionContext*> + properties; + bool has_range = false; + // If has_range is false, lower and upper bound values are not important. + // lower_bound can be larger than upper_bound and in that case there is no + // results. + int64_t lower_bound = 1LL; + int64_t upper_bound = LLONG_MAX; +}; + +struct PatternPart { + std::string output_identifier; + std::vector<Node> nodes; + std::vector<Relationship> relationships; +}; diff --git a/src/query/backend/cpp/cypher_main_visitor.cpp b/src/query/backend/cpp/cypher_main_visitor.cpp new file mode 100644 index 000000000..32326c6ad --- /dev/null +++ b/src/query/backend/cpp/cypher_main_visitor.cpp @@ -0,0 +1,227 @@ +#include "query/backend/cpp/cypher_main_visitor.hpp" + +#include <cassert> +#include <climits> +#include <string> +#include <unordered_map> +#include <utility> +#include <vector> + +#include "query/backend/cpp/compiler_structures.hpp" + +namespace { +// List of unnamed tokens visitor needs to use. This should be reviewed on every +// grammar change since even changes in ordering of rules will cause antlr to +// generate different constants for unnamed tokens. +const auto kDotsTokenId = CypherParser::T__12; // .. +} + +antlrcpp::Any CypherMainVisitor::visitNodePattern( + CypherParser::NodePatternContext *ctx) { + Node node; + node.output_identifier = new_identifier(); + if (ctx->variable()) { + identifiers_map_[ctx->variable()->accept(this).as<std::string>()] = + node.output_identifier; + } + if (ctx->nodeLabels()) { + node.labels = + ctx->nodeLabels()->accept(this).as<std::vector<std::string>>(); + } + if (ctx->properties()) { + node.properties = + ctx->properties() + ->accept(this) + .as<std::unordered_map<std::string, + CypherParser::ExpressionContext *>>(); + } + symbol_table_[node.output_identifier] = node; + return node; +} + +antlrcpp::Any CypherMainVisitor::visitNodeLabels( + CypherParser::NodeLabelsContext *ctx) { + std::vector<std::string> labels; + for (auto *node_label : ctx->nodeLabel()) { + labels.push_back(node_label->accept(this).as<std::string>()); + } + return labels; +} + +antlrcpp::Any CypherMainVisitor::visitProperties( + CypherParser::PropertiesContext *ctx) { + if (!ctx->mapLiteral()) { + // If child is not mapLiteral that means child is params. At the moment + // memgraph doesn't support params. + throw SemanticException(); + } + return ctx->mapLiteral()->accept(this); +} + +antlrcpp::Any CypherMainVisitor::visitMapLiteral( + CypherParser::MapLiteralContext *ctx) { + std::unordered_map<std::string, CypherParser::ExpressionContext *> map; + for (int i = 0; i < (int)ctx->propertyKeyName().size(); ++i) { + map[ctx->propertyKeyName()[i]->accept(this).as<std::string>()] = + ctx->expression()[i]; + } + return map; +} + +antlrcpp::Any CypherMainVisitor::visitSymbolicName( + CypherParser::SymbolicNameContext *ctx) { + if (!ctx->UnescapedSymbolicName()) { + // SymbolicName can only be UnescapedSymbolicName. At this moment we want to + // avoid openCypher crazyness that allows variables to be named as keywords + // and escaped sequences. To allow all possible variable names allowed by + // openCypher grammar we need to figure out escaping rules so we can + // reference same variable as unescaped and escaped string. + throw SemanticException(); + } + return ctx->getText(); +} + +antlrcpp::Any CypherMainVisitor::visitPattern( + CypherParser::PatternContext *ctx) { + std::vector<PatternPart> pattern; + for (auto *pattern_part : ctx->patternPart()) { + pattern.push_back(pattern_part->accept(this).as<PatternPart>()); + } + return pattern; +} + +antlrcpp::Any CypherMainVisitor::visitPatternPart( + CypherParser::PatternPartContext *ctx) { + PatternPart pattern_part = + ctx->anonymousPatternPart()->accept(this).as<PatternPart>(); + if (ctx->variable()) { + identifiers_map_[ctx->variable()->accept(this).as<std::string>()] = + pattern_part.output_identifier; + } + symbol_table_[pattern_part.output_identifier] = pattern_part; + return pattern_part; +} + +antlrcpp::Any CypherMainVisitor::visitPatternElement( + CypherParser::PatternElementContext *ctx) { + if (ctx->patternElement()) { + return ctx->patternElement()->accept(this); + } + PatternPart pattern_part; + pattern_part.output_identifier = new_identifier(); + pattern_part.nodes.push_back(ctx->nodePattern()->accept(this).as<Node>()); + for (auto *pattern_element_chain : ctx->patternElementChain()) { + auto element = + pattern_element_chain->accept(this).as<std::pair<Relationship, Node>>(); + pattern_part.relationships.push_back(element.first); + pattern_part.nodes.push_back(element.second); + } + return pattern_part; +} + +antlrcpp::Any CypherMainVisitor::visitPatternElementChain( + CypherParser::PatternElementChainContext *ctx) { + return std::pair<Relationship, Node>( + ctx->relationshipPattern()->accept(this).as<Relationship>(), + ctx->nodePattern()->accept(this).as<Node>()); +} + +antlrcpp::Any CypherMainVisitor::visitRelationshipPattern( + CypherParser::RelationshipPatternContext *ctx) { + Relationship relationship; + relationship.output_identifier = new_identifier(); + if (ctx->relationshipDetail()) { + VisitRelationshipDetail(ctx->relationshipDetail(), relationship); + } + if (ctx->leftArrowHead() && !ctx->rightArrowHead()) { + relationship.direction = Relationship::Direction::LEFT; + } else if (!ctx->leftArrowHead() && ctx->rightArrowHead()) { + relationship.direction = Relationship::Direction::RIGHT; + } else { + // <-[]-> and -[]- is the same thing as far as we understand openCypher + // grammar. + relationship.direction = Relationship::Direction::BOTH; + } + symbol_table_[relationship.output_identifier] = relationship; + return relationship; +} + +antlrcpp::Any CypherMainVisitor::visitRelationshipDetail( + CypherParser::RelationshipDetailContext *) { + assert(false); + return 0; +} + +void CypherMainVisitor::VisitRelationshipDetail( + CypherParser::RelationshipDetailContext *ctx, Relationship &relationship) { + if (ctx->variable()) { + identifiers_map_[ctx->variable()->accept(this).as<std::string>()] = + relationship.output_identifier; + } + if (ctx->relationshipTypes()) { + relationship.types = + ctx->relationshipTypes()->accept(this).as<std::vector<std::string>>(); + } + if (ctx->properties()) { + relationship.properties = + ctx->properties() + ->accept(this) + .as<std::unordered_map<std::string, + CypherParser::ExpressionContext *>>(); + } + if (ctx->rangeLiteral()) { + relationship.has_range = true; + auto range = + ctx->rangeLiteral()->accept(this).as<std::pair<int64_t, int64_t>>(); + relationship.lower_bound = range.first; + relationship.upper_bound = range.second; + } +} + +antlrcpp::Any CypherMainVisitor::visitRelationshipTypes( + CypherParser::RelationshipTypesContext *ctx) { + std::vector<std::string> types; + for (auto *label : ctx->relTypeName()) { + types.push_back(label->accept(this).as<std::string>()); + } + return types; +} + +antlrcpp::Any CypherMainVisitor::visitRangeLiteral( + CypherParser::RangeLiteralContext *ctx) { + if (ctx->integerLiteral().size() == 0U) { + // -[*]- + return std::pair<int64_t, int64_t>(1LL, LLONG_MAX); + } else if (ctx->integerLiteral().size() == 1U) { + auto dots_tokens = ctx->getTokens(kDotsTokenId); + int64_t bound = ctx->integerLiteral()[0]->accept(this).as<int64_t>(); + if (!dots_tokens.size()) { + // -[*2]- + return std::pair<int64_t, int64_t>(bound, bound); + } + if (dots_tokens[0]->getSourceInterval().startsAfter( + ctx->integerLiteral()[0]->getSourceInterval())) { + // -[*2..]- + return std::pair<int64_t, int64_t>(bound, LLONG_MAX); + } else { + // -[*..2]- + return std::pair<int64_t, int64_t>(1LL, bound); + } + } else { + int64_t lbound = ctx->integerLiteral()[0]->accept(this).as<int64_t>(); + int64_t rbound = ctx->integerLiteral()[1]->accept(this).as<int64_t>(); + // -[*2..5]- + return std::pair<int64_t, int64_t>(lbound, rbound); + } +} + +antlrcpp::Any CypherMainVisitor::visitIntegerLiteral( + CypherParser::IntegerLiteralContext *ctx) { + int64_t t = 0LL; + try { + t = std::stoll(ctx->getText(), 0, 0); + } catch (std::out_of_range) { + throw SemanticException(); + } + return t; +} diff --git a/src/query/backend/cpp/cypher_main_visitor.hpp b/src/query/backend/cpp/cypher_main_visitor.hpp new file mode 100644 index 000000000..f5fc94d69 --- /dev/null +++ b/src/query/backend/cpp/cypher_main_visitor.hpp @@ -0,0 +1,140 @@ +#pragma once + +#include <string> +#include "query/frontend/opencypher/generated/CypherBaseVisitor.h" +#include "antlr4-runtime.h" +#include "query/backend/cpp/compiler_structures.hpp" + +using antlropencypher::CypherParser; + +class CypherMainVisitor : public antlropencypher::CypherBaseVisitor { + /** + * Creates Node and stores it in symbol_table_. If variable is defined it is + * stored in identifiers_map_. + * + * @return Node. + */ + antlrcpp::Any visitNodePattern( + CypherParser::NodePatternContext *ctx) override; + + /** + * @return vector<string> labels. + */ + antlrcpp::Any visitNodeLabels(CypherParser::NodeLabelsContext *ctx) override; + + /** + * @return unordered_map<string, ExpressionContext*> properties. + */ + antlrcpp::Any visitProperties(CypherParser::PropertiesContext *ctx) override; + + /** + * @return unordered_map<string, ExpressionContext*> map. + */ + antlrcpp::Any visitMapLiteral(CypherParser::MapLiteralContext *ctx) override; + + /** + * @return string. + */ + antlrcpp::Any visitSymbolicName( + CypherParser::SymbolicNameContext *ctx) override; + + /** + * @return vector<PatternPart> pattern. + */ + antlrcpp::Any visitPattern(CypherParser::PatternContext *ctx) override; + + /** + * Stores PatternPart in symbol_table_. If variable is defined it is stored in + * identifiers_map_. + * + * @return PatternPart. + */ + antlrcpp::Any visitPatternPart( + CypherParser::PatternPartContext *ctx) override; + + /** + * Creates PatternPart. + * + * @return PatternPart. + */ + antlrcpp::Any visitPatternElement( + CypherParser::PatternElementContext *ctx) override; + + /** + * @return pair<Relationship, Node> + */ + antlrcpp::Any visitPatternElementChain( + CypherParser::PatternElementChainContext *ctx) override; + + /** + * Creates Relationship and stores it in symbol_table_. + * + */ + antlrcpp::Any visitRelationshipPattern( + CypherParser::RelationshipPatternContext *ctx) override; + + /** + * This should never be called. Call VisitRelationshipDetail with already + * created Relationship instead. + */ + antlrcpp::Any visitRelationshipDetail( + CypherParser::RelationshipDetailContext *ctx) override; + + /** + * If variable is defined it is stored in symbol_table_. Relationship is + * filled with properties, types and range if provided. + * Use this instead of antlr generated visitRelationshipDetail with already + * created Relationship. If we should have used visitRelationshipDetail + * (relationshipDetail is optional production in relationshipPattern) then we + * would have needed to return not completely initialised Relationship. + */ + void VisitRelationshipDetail(CypherParser::RelationshipDetailContext *ctx, + Relationship &relationship); + + /** + * @return vector<string>. + */ + antlrcpp::Any visitRelationshipTypes( + CypherParser::RelationshipTypesContext *ctx) override; + + /** + * @return int64_t. + */ + antlrcpp::Any visitIntegerLiteral( + CypherParser::IntegerLiteralContext *ctx) override; + + /** + * @return pair<int64_t, int64_t>. + */ + antlrcpp::Any visitRangeLiteral( + CypherParser::RangeLiteralContext *ctx) override; + + public: + // TODO: These temporary getters should eventually be replaced with something + // else once we figure out where and how those strctures will be used. + // Currently there are needed for testing. cypher_main_visitor test should be + // refactored once these getters are deleted. + const std::unordered_map<std::string, std::string> &identifiers_map() const { + return identifiers_map_; + } + const std::unordered_map<std::string, antlrcpp::Any> &symbol_table() const { + return symbol_table_; + } + + private: + // Return new output code identifier. + // TODO: Should we generate identifiers with more readable names: node_1, + // relationship_5, ...? + std::string new_identifier() const { + static int next_identifier = 0; + return "id" + std::to_string(next_identifier++); + } + + // Mapping of identifiers (nodes, relationships, values, lists ...) from query + // code to identifier that is used in generated code; + std::unordered_map<std::string, std::string> identifiers_map_; + + // Mapping of output (generated) code identifiers to appropriate parser + // structure. + std::unordered_map<std::string, antlrcpp::Any> symbol_table_; +}; diff --git a/src/query/backend/cpp/generator.hpp b/src/query/backend/cpp/generator.hpp index 9cc80f878..0175d2aa6 100644 --- a/src/query/backend/cpp/generator.hpp +++ b/src/query/backend/cpp/generator.hpp @@ -1,25 +1,30 @@ #pragma once #include <experimental/filesystem> +#include "antlr4-runtime.h" +#include "query/backend/cpp/cypher_main_visitor.hpp" + namespace fs = std::experimental::filesystem; namespace backend { namespace cpp { +using namespace antlr4; + /** - * Traverse AST and generate C++ + * Traverse Antlr tree::ParseTree generated from Cypher grammar and generate + * C++. */ class Generator { public: /** * Generates cpp code inside file on the path. - * - * @tparam Ast type of AST structure */ - template <typename Ast> - void generate_plan(const Ast &ast, const std::string &query, - const uint64_t stripped_hash, const fs::path &path) { + Generator(tree::ParseTree *tree, const std::string &query, + const uint64_t stripped_hash, const fs::path &path) { + CypherMainVisitor visitor; + visitor.visit(tree); throw std::runtime_error("TODO: implementation"); } }; diff --git a/src/query/engine.hpp b/src/query/engine.hpp index b319cc61f..cedb49884 100644 --- a/src/query/engine.hpp +++ b/src/query/engine.hpp @@ -11,7 +11,6 @@ namespace fs = std::experimental::filesystem; #include "query/exception/query_engine.hpp" #include "query/frontend/opencypher/parser.hpp" #include "query/plan_compiler.hpp" -#include "query/plan_generator.hpp" #include "query/plan_interface.hpp" #include "query/preprocessor.hpp" #include "utils/dynamic_lib.hpp" @@ -147,7 +146,9 @@ class QueryEngine : public Loggable { auto generated_path = fs::path(CONFIG(config::COMPILE_PATH) + std::to_string(stripped.hash) + ".cpp"); - plan_generator.generate_plan(stripped.query, stripped.hash, generated_path); + frontend::opencypher::Parser parser(stripped.query); + backend::cpp::Generator(parser.tree(), stripped.query, stripped.hash, + generated_path); return LoadCpp(generated_path, stripped.hash); } @@ -193,7 +194,5 @@ class QueryEngine : public Loggable { QueryPreprocessor preprocessor; PlanCompiler plan_compiler; - PlanGenerator<frontend::opencypher::Parser, backend::cpp::Generator> - plan_generator; ConcurrentMap<HashType, std::unique_ptr<QueryPlanLib>> query_plans; }; diff --git a/src/query/frontend/opencypher/parser.hpp b/src/query/frontend/opencypher/parser.hpp index f1d9f1808..cee963d78 100644 --- a/src/query/frontend/opencypher/parser.hpp +++ b/src/query/frontend/opencypher/parser.hpp @@ -5,6 +5,7 @@ #include "antlr4-runtime.h" #include "query/frontend/opencypher/generated/CypherLexer.h" #include "query/frontend/opencypher/generated/CypherParser.h" +#include "utils/exceptions/basic_exception.hpp" namespace frontend { namespace opencypher { @@ -12,8 +13,15 @@ namespace opencypher { using namespace antlropencypher; using namespace antlr4; +class SyntaxException : BasicException { + public: + SyntaxException() : BasicException("") {} +}; + /** * Generates openCypher AST + * This thing must me a class since parser.cypher() returns pointer and there is + * no way for us to get ownership over the object. */ class Parser { public: @@ -21,18 +29,23 @@ class Parser { * @param query incomming query that has to be compiled into query plan * the first step is to generate AST */ - auto generate_ast(const std::string &query) { - // get tokens - ANTLRInputStream input(query.c_str()); - CypherLexer lexer(&input); - CommonTokenStream tokens(&lexer); - - // generate ast - CypherParser parser(&tokens); - tree::ParseTree *tree = parser.cypher(); - - return tree; + Parser(const std::string query) : query_(std::move(query)) { + if (parser_.getNumberOfSyntaxErrors()) { + throw SyntaxException(); + } } + + auto tree() { return tree_; } + + private: + std::string query_; + ANTLRInputStream input_{query_.c_str()}; + CypherLexer lexer_{&input_}; + CommonTokenStream tokens_{&lexer_}; + + // generate ast + CypherParser parser_{&tokens_}; + tree::ParseTree *tree_{parser_.cypher()}; }; } } diff --git a/src/query/plan_generator.hpp b/src/query/plan_generator.hpp deleted file mode 100644 index 366a397ec..000000000 --- a/src/query/plan_generator.hpp +++ /dev/null @@ -1,30 +0,0 @@ -#pragma once - -#include <experimental/filesystem> -namespace fs = std::experimental::filesystem; - -/** - * @class PlanGenerator - * - * @tparam Frontend defines compiler frontend for query parsing - * object of this class must have method with name generate_ast - * @tparam Backend defines compiler backend for plan gen - * object of this class must have method with name generate_code - * - */ -template <typename Frontend, typename Backend> -class PlanGenerator { - public: - /** - * Generates query plan based on the input query - */ - void generate_plan(const std::string &query, const uint64_t stripped_hash, - const fs::path &path) { - auto ast = frontend.generate_ast(query); - backend.generate_plan(ast, query, stripped_hash, path); - } - - private: - Frontend frontend; - Backend backend; -}; diff --git a/tests/unit/cypher_main_visitor.cpp b/tests/unit/cypher_main_visitor.cpp new file mode 100644 index 000000000..b4bb88c14 --- /dev/null +++ b/tests/unit/cypher_main_visitor.cpp @@ -0,0 +1,307 @@ +#include <climits> +#include <string> +#include <vector> +#include <unordered_map> +#include <algorithm> +#include "antlr4-runtime.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "query/backend/cpp/cypher_main_visitor.cpp" +#include "query/frontend/opencypher/parser.hpp" + +using namespace ::testing; + +namespace { + +class ParserTables { + template <typename T> + auto FilterAnies(std::unordered_map<std::string, antlrcpp::Any> map) { + std::unordered_map<std::string, T> filtered; + for (auto x : map) { + if (x.second.is<T>()) { + filtered[x.first] = x.second.as<T>(); + } + } + return filtered; + } + + public: + ParserTables(const std::string &query) { + frontend::opencypher::Parser parser(query); + auto *tree = parser.tree(); + CypherMainVisitor visitor; + visitor.visit(tree); + identifiers_map_ = visitor.identifiers_map(); + symbol_table_ = visitor.symbol_table(); + pattern_parts_ = FilterAnies<PatternPart>(symbol_table_); + nodes_ = FilterAnies<Node>(symbol_table_); + relationships_ = FilterAnies<Relationship>(symbol_table_); + } + + std::unordered_map<std::string, std::string> identifiers_map_; + std::unordered_map<std::string, antlrcpp::Any> symbol_table_; + std::unordered_map<std::string, PatternPart> pattern_parts_; + std::unordered_map<std::string, Node> nodes_; + std::unordered_map<std::string, Relationship> relationships_; +}; + +// TODO: Once expression evaluation is implemented, we should also test if +// property values are equal. +void CompareNodes(std::pair<std::string, Node> node_entry, + std::vector<std::string> labels, + std::vector<std::string> property_keys) { + auto node = node_entry.second; + ASSERT_EQ(node_entry.first, node.output_identifier); + ASSERT_THAT(node.labels, + UnorderedElementsAreArray(labels.begin(), labels.end())); + std::vector<std::string> node_property_keys; + for (auto x : node.properties) { + node_property_keys.push_back(x.first); + } + ASSERT_THAT( + node_property_keys, + UnorderedElementsAreArray(property_keys.begin(), property_keys.end())); +} + +// If has_range is false, lower and upper bound values are ignored. +// TODO: Once expression evaluation is implemented, we should also test if +// property values are equal. +void CompareRelationships( + std::pair<std::string, Relationship> relationship_entry, + Relationship::Direction direction, std::vector<std::string> types, + std::vector<std::string> property_keys, bool has_range, + int64_t lower_bound = 1LL, int64_t upper_bound = LLONG_MAX) { + auto relationship = relationship_entry.second; + ASSERT_EQ(relationship_entry.first, relationship.output_identifier); + ASSERT_EQ(relationship.direction, direction); + ASSERT_THAT(relationship.types, + UnorderedElementsAreArray(types.begin(), types.end())); + std::vector<std::string> relationship_property_keys; + for (auto x : relationship.properties) { + relationship_property_keys.push_back(x.first); + } + ASSERT_THAT( + relationship_property_keys, + UnorderedElementsAreArray(property_keys.begin(), property_keys.end())); + ASSERT_EQ(relationship.has_range, has_range); + if (!has_range) return; + ASSERT_EQ(relationship.lower_bound, lower_bound); + ASSERT_EQ(relationship.upper_bound, upper_bound); +} + +// SyntaxException on incorrect syntax. +TEST(CompilerStructuresTest, SyntaxException) { + ASSERT_THROW(ParserTables("CREATE ()-[*1...2]-()"), + frontend::opencypher::SyntaxException); +} + +// Empty node. +TEST(CompilerStructuresTest, NodePatternEmpty) { + ParserTables parser("CREATE ()"); + ASSERT_EQ(parser.identifiers_map_.size(), 0U); + ASSERT_EQ(parser.nodes_.size(), 1U); + CompareNodes(*parser.nodes_.begin(), {}, {}); +} + +// Node with variable. +TEST(CompilerStructuresTest, NodePatternVariable) { + ParserTables parser("CREATE (var)"); + ASSERT_EQ(parser.identifiers_map_.size(), 1U); + ASSERT_NE(parser.identifiers_map_.find("var"), parser.identifiers_map_.end()); + ASSERT_EQ(parser.nodes_.size(), 1U); + auto output_identifier = parser.identifiers_map_["var"]; + ASSERT_NE(parser.nodes_.find(output_identifier), parser.nodes_.end()); + CompareNodes(*parser.nodes_.begin(), {}, {}); +} + +// Node with labels. +TEST(CompilerStructuresTest, NodePatternLabels) { + ParserTables parser("CREATE (:label1:label2:label3)"); + ASSERT_EQ(parser.identifiers_map_.size(), 0U); + ASSERT_EQ(parser.nodes_.size(), 1U); + CompareNodes(*parser.nodes_.begin(), {"label1", "label2", "label3"}, {}); +} + +// Node with properties. +TEST(CompilerStructuresTest, NodePatternProperties) { + ParserTables parser("CREATE ({age: 5, name: \"John\", surname: \"Smith\"})"); + ASSERT_EQ(parser.identifiers_map_.size(), 0U); + ASSERT_EQ(parser.nodes_.size(), 1U); + CompareNodes(*parser.nodes_.begin(), {}, {"age", "name", "surname"}); +} + +// Relationship without relationship details. +TEST(CompilerStructuresTest, RelationshipPatternNoDetails) { + ParserTables parser("CREATE ()--()"); + ASSERT_EQ(parser.identifiers_map_.size(), 0U); + ASSERT_EQ(parser.relationships_.size(), 1U); + CompareRelationships(*parser.relationships_.begin(), + Relationship::Direction::BOTH, {}, {}, false); +} + +// Relationship with empty relationship details. +TEST(CompilerStructuresTest, RelationshipPatternEmptyDetails) { + ParserTables parser("CREATE ()-[]-()"); + ASSERT_EQ(parser.identifiers_map_.size(), 0U); + ASSERT_EQ(parser.relationships_.size(), 1U); + CompareRelationships(*parser.relationships_.begin(), + Relationship::Direction::BOTH, {}, {}, false); +} + +// Relationship with left direction. +TEST(CompilerStructuresTest, RelationshipPatternLeftDirection) { + ParserTables parser("CREATE ()<--()"); + ASSERT_EQ(parser.identifiers_map_.size(), 0U); + ASSERT_EQ(parser.relationships_.size(), 1U); + CompareRelationships(*parser.relationships_.begin(), + Relationship::Direction::LEFT, {}, {}, false); +} + +// Relationship with right direction. +TEST(CompilerStructuresTest, RelationshipPatternRightDirection) { + ParserTables parser("CREATE ()-[]->()"); + ASSERT_EQ(parser.identifiers_map_.size(), 0U); + ASSERT_EQ(parser.relationships_.size(), 1U); + CompareRelationships(*parser.relationships_.begin(), + Relationship::Direction::RIGHT, {}, {}, false); +} + +// Relationship with both directions. +TEST(CompilerStructuresTest, RelationshipPatternBothDirection) { + ParserTables parser("CREATE ()<-[]->()"); + ASSERT_EQ(parser.identifiers_map_.size(), 0U); + ASSERT_EQ(parser.relationships_.size(), 1U); + CompareRelationships(*parser.relationships_.begin(), + Relationship::Direction::BOTH, {}, {}, false); +} + +// Relationship with unbounded variable range. +TEST(CompilerStructuresTest, RelationshipPatternUnbounded) { + ParserTables parser("CREATE ()-[*]-()"); + ASSERT_EQ(parser.identifiers_map_.size(), 0U); + ASSERT_EQ(parser.relationships_.size(), 1U); + CompareRelationships(*parser.relationships_.begin(), + Relationship::Direction::BOTH, {}, {}, true, 1, + LLONG_MAX); +} + +// Relationship with lower bounded variable range. +TEST(CompilerStructuresTest, RelationshipPatternLowerBounded) { + ParserTables parser("CREATE ()-[*5..]-()"); + ASSERT_EQ(parser.identifiers_map_.size(), 0U); + ASSERT_EQ(parser.relationships_.size(), 1U); + CompareRelationships(*parser.relationships_.begin(), + Relationship::Direction::BOTH, {}, {}, true, 5, + LLONG_MAX); +} + +// Relationship with upper bounded variable range. +TEST(CompilerStructuresTest, RelationshipPatternUpperBounded) { + ParserTables parser("CREATE ()-[*..10]-()"); + ASSERT_EQ(parser.identifiers_map_.size(), 0U); + ASSERT_EQ(parser.relationships_.size(), 1U); + CompareRelationships(*parser.relationships_.begin(), + Relationship::Direction::BOTH, {}, {}, true, 1, 10); +} + +// Relationship with lower and upper bounded variable range. +TEST(CompilerStructuresTest, RelationshipPatternLowerUpperBounded) { + ParserTables parser("CREATE ()-[*5..10]-()"); + ASSERT_EQ(parser.identifiers_map_.size(), 0U); + ASSERT_EQ(parser.relationships_.size(), 1U); + CompareRelationships(*parser.relationships_.begin(), + Relationship::Direction::BOTH, {}, {}, true, 5, 10); +} + +// Relationship with fixed number of edges. +TEST(CompilerStructuresTest, RelationshipPatternFixedRange) { + ParserTables parser("CREATE ()-[*10]-()"); + ASSERT_EQ(parser.identifiers_map_.size(), 0U); + ASSERT_EQ(parser.relationships_.size(), 1U); + CompareRelationships(*parser.relationships_.begin(), + Relationship::Direction::BOTH, {}, {}, true, 10, 10); +} + +// Relationship with invalid bound (larger than long long). +TEST(CompilerStructuresTest, RelationshipPatternInvalidBound) { + ASSERT_THROW( + ParserTables parser("CREATE ()-[*100000000000000000000000000]-()"), + SemanticException); +} + +// Relationship with variable +TEST(CompilerStructuresTest, RelationshipPatternVariable) { + ParserTables parser("CREATE ()-[var]-()"); + ASSERT_EQ(parser.identifiers_map_.size(), 1U); + ASSERT_NE(parser.identifiers_map_.find("var"), parser.identifiers_map_.end()); + ASSERT_EQ(parser.relationships_.size(), 1U); + auto output_identifier = parser.identifiers_map_["var"]; + ASSERT_NE(parser.relationships_.find(output_identifier), + parser.relationships_.end()); + CompareRelationships(*parser.relationships_.begin(), + Relationship::Direction::BOTH, {}, {}, false); +} + +// Relationship with labels. +TEST(CompilerStructuresTest, RelationshipPatternLabels) { + ParserTables parser("CREATE ()-[:label1|label2|:label3]-()"); + ASSERT_EQ(parser.identifiers_map_.size(), 0U); + ASSERT_EQ(parser.relationships_.size(), 1U); + CompareRelationships(*parser.relationships_.begin(), + Relationship::Direction::BOTH, + {"label1", "label2", "label3"}, {}, false); +} + +// Relationship with properties. +TEST(CompilerStructuresTest, RelationshipPatternProperties) { + ParserTables parser( + "CREATE ()-[{age: 5, name: \"John\", surname: \"Smith\"}]-()"); + ASSERT_EQ(parser.identifiers_map_.size(), 0U); + ASSERT_EQ(parser.relationships_.size(), 1U); + CompareRelationships(*parser.relationships_.begin(), + Relationship::Direction::BOTH, {}, + {"age", "name", "surname"}, false); +} + +// PatternPart. +TEST(CompilerStructuresTest, PatternPart) { + ParserTables parser("CREATE ()--()"); + ASSERT_EQ(parser.identifiers_map_.size(), 0U); + ASSERT_EQ(parser.pattern_parts_.size(), 1U); + ASSERT_EQ(parser.relationships_.size(), 1U); + ASSERT_EQ(parser.nodes_.size(), 2U); + ASSERT_EQ(parser.pattern_parts_.begin()->second.nodes.size(), 2U); + ASSERT_EQ(parser.pattern_parts_.begin()->second.relationships.size(), 1U); +} + +// PatternPart in braces. +TEST(CompilerStructuresTest, PatternPartBraces) { + ParserTables parser("CREATE ((()--()))"); + ASSERT_EQ(parser.identifiers_map_.size(), 0U); + ASSERT_EQ(parser.pattern_parts_.size(), 1U); + ASSERT_EQ(parser.relationships_.size(), 1U); + ASSERT_EQ(parser.nodes_.size(), 2U); + ASSERT_EQ(parser.pattern_parts_.begin()->second.nodes.size(), 2U); + ASSERT_EQ(parser.pattern_parts_.begin()->second.relationships.size(), 1U); +} + +// PatternPart with variable. +TEST(CompilerStructuresTest, PatternPartVariable) { + ParserTables parser("CREATE var=()--()"); + ASSERT_EQ(parser.identifiers_map_.size(), 1U); + ASSERT_EQ(parser.pattern_parts_.size(), 1U); + ASSERT_EQ(parser.relationships_.size(), 1U); + ASSERT_EQ(parser.nodes_.size(), 2U); + ASSERT_EQ(parser.pattern_parts_.begin()->second.nodes.size(), 2U); + ASSERT_EQ(parser.pattern_parts_.begin()->second.relationships.size(), 1U); + ASSERT_NE(parser.identifiers_map_.find("var"), parser.identifiers_map_.end()); + auto output_identifier = parser.identifiers_map_["var"]; + ASSERT_NE(parser.pattern_parts_.find(output_identifier), + parser.pattern_parts_.end()); +} +} + +int main(int argc, char **argv) { + InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +}