diff --git a/src/auth/models.cpp b/src/auth/models.cpp index 268b5d25c..d783fba37 100644 --- a/src/auth/models.cpp +++ b/src/auth/models.cpp @@ -40,6 +40,8 @@ std::string PermissionToString(Permission permission) { return "AUTH"; case Permission::STREAM: return "STREAM"; + case Permission::CONSTRAINT: + return "CONSTRAINT"; } } diff --git a/src/auth/models.hpp b/src/auth/models.hpp index 9f9dedf53..0fe86290e 100644 --- a/src/auth/models.hpp +++ b/src/auth/models.hpp @@ -18,6 +18,7 @@ enum class Permission : uint64_t { REMOVE = 0x00000020, INDEX = 0x00000040, STATS = 0x00000080, + CONSTRAINT = 0x00000100, AUTH = 0x00010000, STREAM = 0x00020000, }; diff --git a/src/glue/auth.cpp b/src/glue/auth.cpp index cac5bd113..9d42ae925 100644 --- a/src/glue/auth.cpp +++ b/src/glue/auth.cpp @@ -24,6 +24,8 @@ auth::Permission PrivilegeToPermission(query::AuthQuery::Privilege privilege) { return auth::Permission::AUTH; case query::AuthQuery::Privilege::STREAM: return auth::Permission::STREAM; + case query::AuthQuery::Privilege::CONSTRAINT: + return auth::Permission::CONSTRAINT; } } diff --git a/src/query/frontend/ast/ast.lcp b/src/query/frontend/ast/ast.lcp index 82e79a00e..0bed39d95 100644 --- a/src/query/frontend/ast/ast.lcp +++ b/src/query/frontend/ast/ast.lcp @@ -2402,7 +2402,7 @@ cpp<# show-users-for-role) (:serialize)) (lcp:define-enum privilege - (create delete match merge set remove index stats auth stream) + (create delete match merge set remove index stats auth stream constraint) (:serialize)) #>cpp AuthQuery() = default; @@ -2436,7 +2436,8 @@ const std::vector kPrivilegesAll = { AuthQuery::Privilege::MATCH, AuthQuery::Privilege::MERGE, AuthQuery::Privilege::SET, AuthQuery::Privilege::REMOVE, AuthQuery::Privilege::INDEX, AuthQuery::Privilege::STATS, - AuthQuery::Privilege::AUTH, AuthQuery::Privilege::STREAM}; + AuthQuery::Privilege::AUTH, AuthQuery::Privilege::STREAM, + AuthQuery::Privilege::CONSTRAINT}; cpp<# (lcp:define-class stream-query (query) @@ -2515,7 +2516,47 @@ cpp<# ((info-type "InfoType" :scope :public)) (:public (lcp:define-enum info-type - (storage index) + (storage index constraint) + (:serialize)) + + #>cpp + DEFVISITABLE(QueryVisitor); + cpp<#) + (:serialize (:slk) (:capnp)) + (:clone)) + +(lcp:define-class constraint-query (query) + ((action-type "ActionType" :scope :public) + (label "LabelIx" :scope :public + :slk-load (lambda (member) + #>cpp + slk::Load(&self->${member}, reader, storage); + cpp<#) + :clone (lambda (source dest) + #>cpp + ${dest} = storage->GetLabelIx(${source}.name); + cpp<#)) + (properties "std::vector" :scope :public + :slk-load (lambda (member) + #>cpp + size_t size = 0; + slk::Load(&size, reader); + self->${member}.resize(size); + for (size_t i = 0; i < size; ++i) { + slk::Load(&self->${member}[i], reader, storage); + } + cpp<#) + :capnp-load (lcp:capnp-load-vector + "capnp::PropertyIx" "PropertyIx" + "[storage](const auto &reader) { + PropertyIx ix; + Load(&ix, reader, storage); + return ix; + }") + :clone (clone-name-ix-vector "Property"))) + (:public + (lcp:define-enum action-type + (create drop) (:serialize)) #>cpp diff --git a/src/query/frontend/ast/ast_visitor.hpp b/src/query/frontend/ast/ast_visitor.hpp index 41a41745a..7df00d1a7 100644 --- a/src/query/frontend/ast/ast_visitor.hpp +++ b/src/query/frontend/ast/ast_visitor.hpp @@ -67,6 +67,7 @@ class ProfileQuery; class IndexQuery; class StreamQuery; class InfoQuery; +class ConstraintQuery; using TreeCompositeVisitor = ::utils::CompositeVisitor< SingleQuery, CypherUnion, NamedExpression, OrOperator, XorOperator, @@ -110,6 +111,7 @@ class ExpressionVisitor template class QueryVisitor : public ::utils::Visitor {}; + IndexQuery, AuthQuery, StreamQuery, InfoQuery, + ConstraintQuery> {}; } // namespace query diff --git a/src/query/frontend/ast/cypher_main_visitor.cpp b/src/query/frontend/ast/cypher_main_visitor.cpp index e14bb977d..eba0764cd 100644 --- a/src/query/frontend/ast/cypher_main_visitor.cpp +++ b/src/query/frontend/ast/cypher_main_visitor.cpp @@ -49,7 +49,7 @@ antlrcpp::Any CypherMainVisitor::visitProfileQuery( antlrcpp::Any CypherMainVisitor::visitInfoQuery( MemgraphCypher::InfoQueryContext *ctx) { CHECK(ctx->children.size() == 2) - << "ProfileQuery should have exactly two children!"; + << "InfoQuery should have exactly two children!"; auto *info_query = storage_->Create(); query_ = info_query; if (ctx->storageInfo()) { @@ -58,11 +58,64 @@ antlrcpp::Any CypherMainVisitor::visitInfoQuery( } else if (ctx->indexInfo()) { info_query->info_type_ = InfoQuery::InfoType::INDEX; return info_query; + } else if (ctx->constraintInfo()) { + info_query->info_type_ = InfoQuery::InfoType::CONSTRAINT; + return info_query; } else { throw utils::NotYetImplemented("Info query: '{}'", ctx->getText()); } } +antlrcpp::Any CypherMainVisitor::visitConstraintQuery( + MemgraphCypher::ConstraintQueryContext *ctx) { + CHECK(ctx->children.size() == 1) + << "ConstraintQuery should have exactly one child!"; + query_ = ctx->children[0]->accept(this).as(); + return query_; +} + +antlrcpp::Any CypherMainVisitor::visitCreateConstraint( + MemgraphCypher::CreateConstraintContext *ctx) { + auto *constraint_query = storage_->Create(); + constraint_query->action_type_ = ConstraintQuery::ActionType::CREATE; + std::string node_name = ctx->nodeName->symbolicName()->accept(this); + for (const auto &var_ctx : ctx->variable()) { + std::string var_name = var_ctx->symbolicName()->accept(this); + if (var_name != node_name) { + throw SemanticException("All variables should reference node '{}'.", + node_name); + } + } + constraint_query->label_ = AddLabel(ctx->labelName()->accept(this)); + constraint_query->properties_.reserve(ctx->propertyLookup().size()); + for (const auto &prop_lookup : ctx->propertyLookup()) { + PropertyIx name_key = prop_lookup->propertyKeyName()->accept(this); + constraint_query->properties_.push_back(name_key); + } + return constraint_query; +} + +antlrcpp::Any CypherMainVisitor::visitDropConstraint( + MemgraphCypher::DropConstraintContext *ctx) { + auto *constraint_query = storage_->Create(); + constraint_query->action_type_ = ConstraintQuery::ActionType::DROP; + std::string node_name = ctx->nodeName->symbolicName()->accept(this); + for (const auto &var_ctx : ctx->variable()) { + std::string var_name = var_ctx->symbolicName()->accept(this); + if (var_name != node_name) { + throw SemanticException("All variables should reference node '{}'.", + node_name); + } + } + constraint_query->label_ = AddLabel(ctx->labelName()->accept(this)); + constraint_query->properties_.reserve(ctx->propertyLookup().size()); + for (const auto &prop_lookup : ctx->propertyLookup()) { + PropertyIx name_key = prop_lookup->propertyKeyName()->accept(this); + constraint_query->properties_.push_back(name_key); + } + return constraint_query; +} + antlrcpp::Any CypherMainVisitor::visitCypherQuery( MemgraphCypher::CypherQueryContext *ctx) { auto *cypher_query = storage_->Create(); @@ -495,6 +548,7 @@ antlrcpp::Any CypherMainVisitor::visitPrivilege( if (ctx->STATS()) return AuthQuery::Privilege::STATS; if (ctx->AUTH()) return AuthQuery::Privilege::AUTH; if (ctx->STREAM()) return AuthQuery::Privilege::STREAM; + if (ctx->CONSTRAINT()) return AuthQuery::Privilege::CONSTRAINT; LOG(FATAL) << "Should not get here - unknown privilege!"; } diff --git a/src/query/frontend/ast/cypher_main_visitor.hpp b/src/query/frontend/ast/cypher_main_visitor.hpp index 5e31d7ebe..ff26446a4 100644 --- a/src/query/frontend/ast/cypher_main_visitor.hpp +++ b/src/query/frontend/ast/cypher_main_visitor.hpp @@ -164,6 +164,24 @@ class CypherMainVisitor : public antlropencypher::MemgraphCypherBaseVisitor { */ antlrcpp::Any visitInfoQuery(MemgraphCypher::InfoQueryContext *ctx) override; + /** + * @return ConstraintQuery* + */ + antlrcpp::Any visitConstraintQuery( + MemgraphCypher::ConstraintQueryContext *ctx) override; + + /** + * @return ConstraintQuery* + */ + antlrcpp::Any visitCreateConstraint( + MemgraphCypher::CreateConstraintContext *ctx) override; + + /** + * @return ConstraintQuery* + */ + antlrcpp::Any visitDropConstraint( + MemgraphCypher::DropConstraintContext *ctx) override; + /** * @return AuthQuery* */ diff --git a/src/query/frontend/opencypher/grammar/Cypher.g4 b/src/query/frontend/opencypher/grammar/Cypher.g4 index 0420c2cc6..bb5d83870 100644 --- a/src/query/frontend/opencypher/grammar/Cypher.g4 +++ b/src/query/frontend/opencypher/grammar/Cypher.g4 @@ -28,13 +28,24 @@ query : cypherQuery | explainQuery | profileQuery | infoQuery + | constraintQuery ; +constraintQuery : createConstraint | dropConstraint ; + +createConstraint : CREATE CONSTRAINT ON '(' nodeName=variable ':' labelName ')' + ASSERT EXISTS '(' variable propertyLookup ( ',' variable propertyLookup )* ')' ; + +dropConstraint : DROP CONSTRAINT ON '(' nodeName=variable ':' labelName ')' + ASSERT EXISTS '(' variable propertyLookup ( ',' variable propertyLookup )* ')' ; + storageInfo : STORAGE INFO ; indexInfo : INDEX INFO ; -infoQuery : SHOW ( storageInfo | indexInfo ) ; +constraintInfo : CONSTRAINT INFO ; + +infoQuery : SHOW ( storageInfo | indexInfo | constraintInfo ) ; explainQuery : EXPLAIN cypherQuery ; @@ -297,9 +308,11 @@ cypherKeyword : ALL | AS | ASC | ASCENDING + | ASSERT | BFS | BY | CASE + | CONSTRAINT | CONTAINS | COUNT | CREATE @@ -312,12 +325,14 @@ cypherKeyword : ALL | ELSE | END | ENDS + | EXISTS | EXPLAIN | EXTRACT | FALSE | FILTER | IN | INDEX + | INFO | IS | LIMIT | L_SKIP @@ -337,6 +352,7 @@ cypherKeyword : ALL | SHOW | SINGLE | STARTS + | STORAGE | THEN | TRUE | UNION diff --git a/src/query/frontend/opencypher/grammar/CypherLexer.g4 b/src/query/frontend/opencypher/grammar/CypherLexer.g4 index 21ccd4a64..e93321630 100644 --- a/src/query/frontend/opencypher/grammar/CypherLexer.g4 +++ b/src/query/frontend/opencypher/grammar/CypherLexer.g4 @@ -74,10 +74,12 @@ ANY : A N Y ; AS : A S ; ASC : A S C ; ASCENDING : A S C E N D I N G ; +ASSERT : A S S E R T ; BFS : B F S ; BY : B Y ; CASE : C A S E ; COALESCE : C O A L E S C E ; +CONSTRAINT : C O N S T R A I N T ; CONTAINS : C O N T A I N S ; COUNT : C O U N T ; CREATE : C R E A T E ; @@ -91,6 +93,7 @@ DROP : D R O P ; ELSE : E L S E ; END : E N D ; ENDS : E N D S ; +EXISTS : E X I S T S ; EXPLAIN : E X P L A I N ; EXTRACT : E X T R A C T ; FALSE : F A L S E ; diff --git a/src/query/frontend/opencypher/grammar/MemgraphCypher.g4 b/src/query/frontend/opencypher/grammar/MemgraphCypher.g4 index fe1a3affb..dc331205f 100644 --- a/src/query/frontend/opencypher/grammar/MemgraphCypher.g4 +++ b/src/query/frontend/opencypher/grammar/MemgraphCypher.g4 @@ -30,6 +30,7 @@ memgraphCypherKeyword : cypherKeyword | ROLES | SIZE | START + | STATS | STOP | STREAM | STREAMS @@ -50,6 +51,7 @@ query : cypherQuery | explainQuery | profileQuery | infoQuery + | constraintQuery | authQuery | streamQuery ; @@ -99,7 +101,7 @@ denyPrivilege : DENY ( ALL PRIVILEGES | privileges=privilegeList ) TO userOrRole revokePrivilege : REVOKE ( ALL PRIVILEGES | privileges=privilegeList ) FROM userOrRole=userOrRoleName ; privilege : CREATE | DELETE | MATCH | MERGE | SET - | REMOVE | INDEX | STATS | AUTH | STREAM ; + | REMOVE | INDEX | STATS | AUTH | STREAM | CONSTRAINT ; privilegeList : privilege ( ',' privilege )* ; diff --git a/src/query/frontend/semantic/required_privileges.cpp b/src/query/frontend/semantic/required_privileges.cpp index 5df081435..d24bb1611 100644 --- a/src/query/frontend/semantic/required_privileges.cpp +++ b/src/query/frontend/semantic/required_privileges.cpp @@ -40,9 +40,18 @@ class PrivilegeExtractor : public QueryVisitor, case InfoQuery::InfoType::STORAGE: AddPrivilege(AuthQuery::Privilege::STATS); break; + case InfoQuery::InfoType::CONSTRAINT: + // TODO: This should be CONSTRAINT | STATS, but we don't have support + // for *or* with privileges. + AddPrivilege(AuthQuery::Privilege::CONSTRAINT); + break; } } + void Visit(ConstraintQuery &constraint_query) override { + AddPrivilege(AuthQuery::Privilege::CONSTRAINT); + } + void Visit(CypherQuery &query) override { query.single_query_->Accept(*this); for (auto *cypher_union : query.cypher_unions_) { diff --git a/src/query/frontend/stripped_lexer_constants.hpp b/src/query/frontend/stripped_lexer_constants.hpp index 7581b5bcc..a7b73e815 100644 --- a/src/query/frontend/stripped_lexer_constants.hpp +++ b/src/query/frontend/stripped_lexer_constants.hpp @@ -91,7 +91,7 @@ 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"}; + "storage", "index", "info", "exists" "assert", "constraint"}; // 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 d7d228582..9308912c4 100644 --- a/src/query/interpreter.cpp +++ b/src/query/interpreter.cpp @@ -613,6 +613,33 @@ Callback HandleInfoQuery(InfoQuery *info_query, database::GraphDbAccessor *db_ac return results; }; break; + case InfoQuery::InfoType::CONSTRAINT: + throw utils::NotYetImplemented("constraint info"); + break; + } + return callback; +} + +Callback HandleConstraintQuery(ConstraintQuery *constraint_query, + database::GraphDbAccessor *db_accessor) { + Callback callback; + std::vector property_names; + property_names.reserve(constraint_query->properties_.size()); + for (const auto &prop_ix : constraint_query->properties_) { + property_names.push_back(prop_ix.name); + } + std::string label_name = constraint_query->label_.name; + switch (constraint_query->action_type_) { + case ConstraintQuery::ActionType::CREATE: + throw utils::NotYetImplemented("create constraint :{}({}) exists", + label_name, + utils::Join(property_names, ", ")); + break; + case ConstraintQuery::ActionType::DROP: + throw utils::NotYetImplemented("drop constraint :{}({}) exists", + label_name, + utils::Join(property_names, ", ")); + break; } return callback; } @@ -849,8 +876,12 @@ Interpreter::Results Interpreter::operator()( } callback = HandleStreamQuery(stream_query, kafka_streams_, parameters, &db_accessor); - } else if (auto *info_query = utils::Downcast(parsed_query.query)) { + } else if (auto *info_query = + utils::Downcast(parsed_query.query)) { callback = HandleInfoQuery(info_query, &db_accessor); + } else if (auto *constraint_query = + utils::Downcast(parsed_query.query)) { + callback = HandleConstraintQuery(constraint_query, &db_accessor); } else { LOG(FATAL) << "Should not get here -- unknown query type!"; } diff --git a/tests/unit/cypher_main_visitor.cpp b/tests/unit/cypher_main_visitor.cpp index f46b388e0..4acc3a1f7 100644 --- a/tests/unit/cypher_main_visitor.cpp +++ b/tests/unit/cypher_main_visitor.cpp @@ -2508,4 +2508,78 @@ TYPED_TEST(CypherMainVisitorTest, TestShowIndexInfo) { } } +TYPED_TEST(CypherMainVisitorTest, TestShowConstraintInfo) { + { + TypeParam ast_generator("SHOW CONSTRAINT INFO"); + auto *query = dynamic_cast(ast_generator.query_); + ASSERT_TRUE(query); + EXPECT_EQ(query->info_type_, InfoQuery::InfoType::CONSTRAINT); + } +} + +TYPED_TEST(CypherMainVisitorTest, CreateConstraintSyntaxError) { + EXPECT_THROW( + TypeParam ast_generator("CREATE CONSTRAINT ON (:label) ASSERT EXISTS"), + SyntaxException); + EXPECT_THROW(TypeParam ast_generator("CREATE CONSTRAINT () ASSERT EXISTS"), + SyntaxException); + EXPECT_THROW( + TypeParam ast_generator("CREATE CONSTRAINT ON () ASSERT EXISTS(prop1)"), + SyntaxException); + EXPECT_THROW(TypeParam ast_generator( + "CREATE CONSTRAINT ON () ASSERT EXISTS (prop1, prop2)"), + SyntaxException); + EXPECT_THROW(TypeParam ast_generator("CREATE CONSTRAINT ON (n:label) ASSERT " + "EXISTS (n.prop1, missing.prop2)"), + SemanticException); +} + +TYPED_TEST(CypherMainVisitorTest, CreateConstraint) { + { + TypeParam ast_generator( + "CREATE CONSTRAINT ON (n:label) ASSERT EXISTS(n.prop1)"); + auto *query = dynamic_cast(ast_generator.query_); + ASSERT_TRUE(query); + EXPECT_EQ(query->action_type_, ConstraintQuery::ActionType::CREATE); + EXPECT_EQ(query->label_, ast_generator.Label("label")); + EXPECT_THAT(query->properties_, + UnorderedElementsAre(ast_generator.Prop("prop1"))); + } + { + TypeParam ast_generator( + "CREATE CONSTRAINT ON (n:label) ASSERT EXISTS (n.prop1, n.prop2)"); + auto *query = dynamic_cast(ast_generator.query_); + ASSERT_TRUE(query); + EXPECT_EQ(query->action_type_, ConstraintQuery::ActionType::CREATE); + EXPECT_EQ(query->label_, ast_generator.Label("label")); + EXPECT_THAT(query->properties_, + UnorderedElementsAre(ast_generator.Prop("prop1"), + ast_generator.Prop("prop2"))); + } +} + +TYPED_TEST(CypherMainVisitorTest, DropConstraint) { + { + TypeParam ast_generator( + "DROP CONSTRAINT ON (n:label) ASSERT EXISTS(n.prop1)"); + auto *query = dynamic_cast(ast_generator.query_); + ASSERT_TRUE(query); + EXPECT_EQ(query->action_type_, ConstraintQuery::ActionType::DROP); + EXPECT_EQ(query->label_, ast_generator.Label("label")); + EXPECT_THAT(query->properties_, + UnorderedElementsAre(ast_generator.Prop("prop1"))); + } + { + TypeParam ast_generator( + "DROP CONSTRAINT ON (n:label) ASSERT EXISTS(n.prop1, n.prop2)"); + auto *query = dynamic_cast(ast_generator.query_); + ASSERT_TRUE(query); + EXPECT_EQ(query->action_type_, ConstraintQuery::ActionType::DROP); + EXPECT_EQ(query->label_, ast_generator.Label("label")); + EXPECT_THAT(query->properties_, + UnorderedElementsAre(ast_generator.Prop("prop1"), + ast_generator.Prop("prop2"))); + } +} + } // namespace diff --git a/tests/unit/query_required_privileges.cpp b/tests/unit/query_required_privileges.cpp index 13387d820..8977fe774 100644 --- a/tests/unit/query_required_privileges.cpp +++ b/tests/unit/query_required_privileges.cpp @@ -147,3 +147,30 @@ TEST_F(TestPrivilegeExtractor, ShowStatsInfo) { UnorderedElementsAre(AuthQuery::Privilege::STATS)); } +TEST_F(TestPrivilegeExtractor, ShowConstraintInfo) { + auto *query = storage.Create(); + query->info_type_ = InfoQuery::InfoType::CONSTRAINT; + EXPECT_THAT(GetRequiredPrivileges(query), + UnorderedElementsAre(AuthQuery::Privilege::CONSTRAINT)); +} + +TEST_F(TestPrivilegeExtractor, CreateConstraint) { + auto *query = storage.Create(); + query->action_type_ = ConstraintQuery::ActionType::CREATE; + query->label_ = storage.GetLabelIx("label"); + query->properties_.push_back(storage.GetPropertyIx("prop0")); + query->properties_.push_back(storage.GetPropertyIx("prop1")); + EXPECT_THAT(GetRequiredPrivileges(query), + UnorderedElementsAre(AuthQuery::Privilege::CONSTRAINT)); +} + +TEST_F(TestPrivilegeExtractor, DropConstraint) { + auto *query = storage.Create(); + query->action_type_ = ConstraintQuery::ActionType::DROP; + query->label_ = storage.GetLabelIx("label"); + query->properties_.push_back(storage.GetPropertyIx("prop0")); + query->properties_.push_back(storage.GetPropertyIx("prop1")); + EXPECT_THAT(GetRequiredPrivileges(query), + UnorderedElementsAre(AuthQuery::Privilege::CONSTRAINT)); +} +