diff --git a/src/query/frontend/ast/ast.lcp b/src/query/frontend/ast/ast.lcp index 13491b9e5..07fc0b9f3 100644 --- a/src/query/frontend/ast/ast.lcp +++ b/src/query/frontend/ast/ast.lcp @@ -2590,7 +2590,7 @@ cpp<# }") :clone (clone-name-ix-vector "Property"))) (:public - (lcp:define-enum type (exists unique) + (lcp:define-enum type (exists unique node-key) (:serialize (:lcp) (:capnp)))) (:serialize (:slk :load-args '((storage "query::AstStorage *"))) (:capnp :load-args '((storage "AstStorage *")))) diff --git a/src/query/frontend/ast/cypher_main_visitor.cpp b/src/query/frontend/ast/cypher_main_visitor.cpp index b9611a09f..c8a4a251a 100644 --- a/src/query/frontend/ast/cypher_main_visitor.cpp +++ b/src/query/frontend/ast/cypher_main_visitor.cpp @@ -87,11 +87,13 @@ antlrcpp::Any CypherMainVisitor::visitConstraintQuery( antlrcpp::Any CypherMainVisitor::visitConstraint( MemgraphCypher::ConstraintContext *ctx) { Constraint constraint; - CHECK(ctx->EXISTS() || ctx->UNIQUE()); + CHECK(ctx->EXISTS() || ctx->UNIQUE() || (ctx->NODE() && ctx->KEY())); if (ctx->EXISTS()) { constraint.type = Constraint::Type::EXISTS; } else if (ctx->UNIQUE()) { constraint.type = Constraint::Type::UNIQUE; + } else if (ctx->NODE() && ctx->KEY()) { + constraint.type = Constraint::Type::NODE_KEY; } constraint.label = AddLabel(ctx->labelName()->accept(this)); std::string node_name = ctx->nodeName->symbolicName()->accept(this); diff --git a/src/query/frontend/opencypher/grammar/Cypher.g4 b/src/query/frontend/opencypher/grammar/Cypher.g4 index 51a460257..d060f55fc 100644 --- a/src/query/frontend/opencypher/grammar/Cypher.g4 +++ b/src/query/frontend/opencypher/grammar/Cypher.g4 @@ -31,10 +31,11 @@ query : cypherQuery | constraintQuery ; -constraintQuery : ( CREATE | DROP ) constraint ; +constraintQuery : ( CREATE | DROP ) CONSTRAINT ON constraint ; -constraint : CONSTRAINT ON '(' nodeName=variable ':' labelName ')' ASSERT EXISTS '(' constraintPropertyList ')' - | CONSTRAINT ON '(' nodeName=variable ':' labelName ')' ASSERT constraintPropertyList IS UNIQUE +constraint : '(' nodeName=variable ':' labelName ')' ASSERT EXISTS '(' constraintPropertyList ')' + | '(' nodeName=variable ':' labelName ')' ASSERT constraintPropertyList IS UNIQUE + | '(' nodeName=variable ':' labelName ')' ASSERT '(' constraintPropertyList ')' IS NODE KEY ; constraintPropertyList : variable propertyLookup ( ',' variable propertyLookup )* ; @@ -336,10 +337,12 @@ cypherKeyword : ALL | INDEX | INFO | IS + | KEY | LIMIT | L_SKIP | MATCH | MERGE + | NODE | NONE | NOT | ON diff --git a/src/query/frontend/opencypher/grammar/CypherLexer.g4 b/src/query/frontend/opencypher/grammar/CypherLexer.g4 index 38ad6736b..214326b88 100644 --- a/src/query/frontend/opencypher/grammar/CypherLexer.g4 +++ b/src/query/frontend/opencypher/grammar/CypherLexer.g4 @@ -102,10 +102,12 @@ IN : I N ; INDEX : I N D E X ; INFO : I N F O ; IS : I S ; +KEY : K E Y ; LIMIT : L I M I T ; L_SKIP : S K I P ; MATCH : M A T C H ; MERGE : M E R G E ; +NODE : N O D E ; NONE : N O N E ; NOT : N O T ; ON : O N ; diff --git a/src/query/frontend/stripped_lexer_constants.hpp b/src/query/frontend/stripped_lexer_constants.hpp index a7b73e815..777bd7bb6 100644 --- a/src/query/frontend/stripped_lexer_constants.hpp +++ b/src/query/frontend/stripped_lexer_constants.hpp @@ -91,7 +91,8 @@ const trie::Trie kKeywords = { "stream", "streams", "load", "data", "kafka", "transform", "batch", "interval", "show", "start", "stats", "stop", "size", "topic", "test", "unique", "explain", "profile", - "storage", "index", "info", "exists" "assert", "constraint"}; + "storage", "index", "info", "exists" "assert", "constraint", + "node", "key"}; // Unicode codepoints that are allowed at the start of the unescaped name. const std::bitset kUnescapedNameAllowedStarts(std::string( diff --git a/src/query/interpreter.cpp b/src/query/interpreter.cpp index fb59e0971..92b1b1890 100644 --- a/src/query/interpreter.cpp +++ b/src/query/interpreter.cpp @@ -665,6 +665,9 @@ Callback HandleConstraintQuery(ConstraintQuery *constraint_query, case Constraint::Type::UNIQUE: type = "unique"; break; + case Constraint::Type::NODE_KEY: + type = "node key"; + break; } switch (constraint_query->action_type_) { case ConstraintQuery::ActionType::CREATE: diff --git a/tests/unit/cypher_main_visitor.cpp b/tests/unit/cypher_main_visitor.cpp index e38b4389b..2d7514895 100644 --- a/tests/unit/cypher_main_visitor.cpp +++ b/tests/unit/cypher_main_visitor.cpp @@ -2700,6 +2700,7 @@ TEST_P(CypherMainVisitorTest, CreateConstraintSyntaxError) { EXPECT_THROW(ast_generator.ParseQuery("CREATE CONSTRAINT ON (n:label) ASSERT " "EXISTS (m.prop1, m.prop2)"), SemanticException); + EXPECT_THROW(ast_generator.ParseQuery( "CREATE CONSTRAINT ON (:label) ASSERT IS UNIQUE"), SyntaxException); @@ -2718,6 +2719,31 @@ TEST_P(CypherMainVisitorTest, CreateConstraintSyntaxError) { EXPECT_THROW(ast_generator.ParseQuery("CREATE CONSTRAINT ON (n:label) ASSERT " "m.prop1, m.prop2 IS UNIQUE"), SemanticException); + + EXPECT_THROW(ast_generator.ParseQuery( + "CREATE CONSTRAINT ON (:label) ASSERT IS NODE KEY"), + SyntaxException); + EXPECT_THROW( + ast_generator.ParseQuery("CREATE CONSTRAINT () ASSERT IS NODE KEY"), + SyntaxException); + EXPECT_THROW(ast_generator.ParseQuery( + "CREATE CONSTRAINT ON () ASSERT (prop1) IS NODE KEY"), + SyntaxException); + EXPECT_THROW(ast_generator.ParseQuery( + "CREATE CONSTRAINT ON () ASSERT (prop1, prop2) IS NODE KEY"), + SyntaxException); + EXPECT_THROW(ast_generator.ParseQuery("CREATE CONSTRAINT ON (n:label) ASSERT " + "(n.prop1, missing.prop2) IS NODE KEY"), + SemanticException); + EXPECT_THROW(ast_generator.ParseQuery("CREATE CONSTRAINT ON (n:label) ASSERT " + "(m.prop1, m.prop2) IS NODE KEY"), + SemanticException); + EXPECT_THROW(ast_generator.ParseQuery("CREATE CONSTRAINT ON (n:label) ASSERT " + "n.prop1, n.prop2 IS NODE KEY"), + SyntaxException); + EXPECT_THROW(ast_generator.ParseQuery("CREATE CONSTRAINT ON (n:label) ASSERT " + "exists(n.prop1, n.prop2) IS NODE KEY"), + SyntaxException); } TEST_P(CypherMainVisitorTest, CreateConstraint) { @@ -2767,6 +2793,30 @@ TEST_P(CypherMainVisitorTest, CreateConstraint) { UnorderedElementsAre(ast_generator.Prop("prop1"), ast_generator.Prop("prop2"))); } + { + auto &ast_generator = *GetParam(); + auto *query = dynamic_cast(ast_generator.ParseQuery( + "CREATE CONSTRAINT ON (n:label) ASSERT (n.prop1) IS NODE KEY")); + ASSERT_TRUE(query); + EXPECT_EQ(query->action_type_, ConstraintQuery::ActionType::CREATE); + EXPECT_EQ(query->constraint_.type, Constraint::Type::NODE_KEY); + EXPECT_EQ(query->constraint_.label, ast_generator.Label("label")); + EXPECT_THAT(query->constraint_.properties, + UnorderedElementsAre(ast_generator.Prop("prop1"))); + } + { + auto &ast_generator = *GetParam(); + auto *query = dynamic_cast( + ast_generator.ParseQuery("CREATE CONSTRAINT ON (n:label) ASSERT " + "(n.prop1, n.prop2) IS NODE KEY")); + ASSERT_TRUE(query); + EXPECT_EQ(query->action_type_, ConstraintQuery::ActionType::CREATE); + EXPECT_EQ(query->constraint_.type, Constraint::Type::NODE_KEY); + EXPECT_EQ(query->constraint_.label, ast_generator.Label("label")); + EXPECT_THAT(query->constraint_.properties, + UnorderedElementsAre(ast_generator.Prop("prop1"), + ast_generator.Prop("prop2"))); + } } TEST_P(CypherMainVisitorTest, DropConstraint) { @@ -2816,6 +2866,30 @@ TEST_P(CypherMainVisitorTest, DropConstraint) { UnorderedElementsAre(ast_generator.Prop("prop1"), ast_generator.Prop("prop2"))); } + { + auto &ast_generator = *GetParam(); + auto *query = dynamic_cast(ast_generator.ParseQuery( + "DROP CONSTRAINT ON (n:label) ASSERT (n.prop1) IS NODE KEY")); + ASSERT_TRUE(query); + EXPECT_EQ(query->action_type_, ConstraintQuery::ActionType::DROP); + EXPECT_EQ(query->constraint_.type, Constraint::Type::NODE_KEY); + EXPECT_EQ(query->constraint_.label, ast_generator.Label("label")); + EXPECT_THAT(query->constraint_.properties, + UnorderedElementsAre(ast_generator.Prop("prop1"))); + } + { + auto &ast_generator = *GetParam(); + auto *query = dynamic_cast( + ast_generator.ParseQuery("DROP CONSTRAINT ON (n:label) ASSERT " + "(n.prop1, n.prop2) IS NODE KEY")); + ASSERT_TRUE(query); + EXPECT_EQ(query->action_type_, ConstraintQuery::ActionType::DROP); + EXPECT_EQ(query->constraint_.type, Constraint::Type::NODE_KEY); + EXPECT_EQ(query->constraint_.label, ast_generator.Label("label")); + EXPECT_THAT(query->constraint_.properties, + UnorderedElementsAre(ast_generator.Prop("prop1"), + ast_generator.Prop("prop2"))); + } } TEST_P(CypherMainVisitorTest, RegexMatch) {