Add syntax for managing data constraints

Reviewers: mtomic, mferencevic, buda, msantl

Reviewed By: mtomic, msantl

Subscribers: pullbot

Differential Revision: https://phabricator.memgraph.io/D1879
This commit is contained in:
Teon Banek 2019-02-22 15:02:31 +01:00
parent 7be23896c2
commit 4d1d9fb15a
15 changed files with 291 additions and 9 deletions

View File

@ -40,6 +40,8 @@ std::string PermissionToString(Permission permission) {
return "AUTH";
case Permission::STREAM:
return "STREAM";
case Permission::CONSTRAINT:
return "CONSTRAINT";
}
}

View File

@ -18,6 +18,7 @@ enum class Permission : uint64_t {
REMOVE = 0x00000020,
INDEX = 0x00000040,
STATS = 0x00000080,
CONSTRAINT = 0x00000100,
AUTH = 0x00010000,
STREAM = 0x00020000,
};

View File

@ -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;
}
}

View File

@ -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<AuthQuery::Privilege> 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<void>);
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<PropertyIx>" :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

View File

@ -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 TResult>
class QueryVisitor
: public ::utils::Visitor<TResult, CypherQuery, ExplainQuery, ProfileQuery,
IndexQuery, AuthQuery, StreamQuery, InfoQuery> {};
IndexQuery, AuthQuery, StreamQuery, InfoQuery,
ConstraintQuery> {};
} // namespace query

View File

@ -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<InfoQuery>();
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<ConstraintQuery *>();
return query_;
}
antlrcpp::Any CypherMainVisitor::visitCreateConstraint(
MemgraphCypher::CreateConstraintContext *ctx) {
auto *constraint_query = storage_->Create<ConstraintQuery>();
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<ConstraintQuery>();
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<CypherQuery>();
@ -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!";
}

View File

@ -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*
*/

View File

@ -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

View File

@ -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 ;

View File

@ -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 )* ;

View File

@ -40,9 +40,18 @@ class PrivilegeExtractor : public QueryVisitor<void>,
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_) {

View File

@ -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<kBitsetSize> kUnescapedNameAllowedStarts(std::string(

View File

@ -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<std::string> 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<InfoQuery>(parsed_query.query)) {
} else if (auto *info_query =
utils::Downcast<InfoQuery>(parsed_query.query)) {
callback = HandleInfoQuery(info_query, &db_accessor);
} else if (auto *constraint_query =
utils::Downcast<ConstraintQuery>(parsed_query.query)) {
callback = HandleConstraintQuery(constraint_query, &db_accessor);
} else {
LOG(FATAL) << "Should not get here -- unknown query type!";
}

View File

@ -2508,4 +2508,78 @@ TYPED_TEST(CypherMainVisitorTest, TestShowIndexInfo) {
}
}
TYPED_TEST(CypherMainVisitorTest, TestShowConstraintInfo) {
{
TypeParam ast_generator("SHOW CONSTRAINT INFO");
auto *query = dynamic_cast<InfoQuery *>(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<ConstraintQuery *>(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<ConstraintQuery *>(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<ConstraintQuery *>(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<ConstraintQuery *>(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

View File

@ -147,3 +147,30 @@ TEST_F(TestPrivilegeExtractor, ShowStatsInfo) {
UnorderedElementsAre(AuthQuery::Privilege::STATS));
}
TEST_F(TestPrivilegeExtractor, ShowConstraintInfo) {
auto *query = storage.Create<InfoQuery>();
query->info_type_ = InfoQuery::InfoType::CONSTRAINT;
EXPECT_THAT(GetRequiredPrivileges(query),
UnorderedElementsAre(AuthQuery::Privilege::CONSTRAINT));
}
TEST_F(TestPrivilegeExtractor, CreateConstraint) {
auto *query = storage.Create<ConstraintQuery>();
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<ConstraintQuery>();
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));
}