diff --git a/CHANGELOG.md b/CHANGELOG.md index e71d56ba8..ec566a181 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Change Log +## Next Release + +### Breaking Changes + +* `indexInfo()` function replaced with `SHOW INDEX INFO` syntax. + +### Major Features and Improvements + +* [Enterprise Ed.] Add new privilege, `STATS` for accessing storage info. +* Add `SHOW STORAGE INFO` feature. + ## v0.14.0 ### Breaking Changes diff --git a/src/auth/models.cpp b/src/auth/models.cpp index 1d3d45c21..7810e1b4f 100644 --- a/src/auth/models.cpp +++ b/src/auth/models.cpp @@ -33,6 +33,8 @@ std::string PermissionToString(Permission permission) { return "REMOVE"; case Permission::INDEX: return "INDEX"; + case Permission::STATS: + return "STATS"; case Permission::AUTH: return "AUTH"; case Permission::STREAM: diff --git a/src/auth/models.hpp b/src/auth/models.hpp index c9ce01183..9f9dedf53 100644 --- a/src/auth/models.hpp +++ b/src/auth/models.hpp @@ -17,6 +17,7 @@ enum class Permission : uint64_t { SET = 0x00000010, REMOVE = 0x00000020, INDEX = 0x00000040, + STATS = 0x00000080, AUTH = 0x00010000, STREAM = 0x00020000, }; @@ -25,7 +26,8 @@ enum class Permission : uint64_t { const std::vector kPermissionsAll = { Permission::MATCH, Permission::CREATE, Permission::MERGE, Permission::DELETE, Permission::SET, Permission::REMOVE, - Permission::INDEX, Permission::AUTH, Permission::STREAM}; + Permission::INDEX, Permission::STATS, Permission::AUTH, + Permission::STREAM}; // Function that converts a permission to its string representation. std::string PermissionToString(Permission permission); diff --git a/src/glue/auth.cpp b/src/glue/auth.cpp index d1ae861b2..cac5bd113 100644 --- a/src/glue/auth.cpp +++ b/src/glue/auth.cpp @@ -18,6 +18,8 @@ auth::Permission PrivilegeToPermission(query::AuthQuery::Privilege privilege) { return auth::Permission::REMOVE; case query::AuthQuery::Privilege::INDEX: return auth::Permission::INDEX; + case query::AuthQuery::Privilege::STATS: + return auth::Permission::STATS; case query::AuthQuery::Privilege::AUTH: return auth::Permission::AUTH; case query::AuthQuery::Privilege::STREAM: diff --git a/src/query/frontend/ast/ast.lcp b/src/query/frontend/ast/ast.lcp index 398ae1601..82e79a00e 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 auth stream) + (create delete match merge set remove index stats auth stream) (:serialize)) #>cpp AuthQuery() = default; @@ -2435,8 +2435,8 @@ const std::vector kPrivilegesAll = { AuthQuery::Privilege::CREATE, AuthQuery::Privilege::DELETE, AuthQuery::Privilege::MATCH, AuthQuery::Privilege::MERGE, AuthQuery::Privilege::SET, AuthQuery::Privilege::REMOVE, - AuthQuery::Privilege::INDEX, AuthQuery::Privilege::AUTH, - AuthQuery::Privilege::STREAM}; + AuthQuery::Privilege::INDEX, AuthQuery::Privilege::STATS, + AuthQuery::Privilege::AUTH, AuthQuery::Privilege::STREAM}; cpp<# (lcp:define-class stream-query (query) @@ -2511,4 +2511,17 @@ cpp<# (:serialize (:slk) (:capnp)) (:clone)) +(lcp:define-class info-query (query) + ((info-type "InfoType" :scope :public)) + (:public + (lcp:define-enum info-type + (storage index) + (:serialize)) + + #>cpp + DEFVISITABLE(QueryVisitor); + cpp<#) + (:serialize (:slk) (:capnp)) + (:clone)) + (lcp:pop-namespace) ;; namespace query diff --git a/src/query/frontend/ast/ast_visitor.hpp b/src/query/frontend/ast/ast_visitor.hpp index 50f57f852..41a41745a 100644 --- a/src/query/frontend/ast/ast_visitor.hpp +++ b/src/query/frontend/ast/ast_visitor.hpp @@ -66,6 +66,7 @@ class ExplainQuery; class ProfileQuery; class IndexQuery; class StreamQuery; +class InfoQuery; using TreeCompositeVisitor = ::utils::CompositeVisitor< SingleQuery, CypherUnion, NamedExpression, OrOperator, XorOperator, @@ -109,6 +110,6 @@ class ExpressionVisitor template class QueryVisitor : public ::utils::Visitor {}; + IndexQuery, AuthQuery, StreamQuery, InfoQuery> {}; } // namespace query diff --git a/src/query/frontend/ast/cypher_main_visitor.cpp b/src/query/frontend/ast/cypher_main_visitor.cpp index 96561fbed..e14bb977d 100644 --- a/src/query/frontend/ast/cypher_main_visitor.cpp +++ b/src/query/frontend/ast/cypher_main_visitor.cpp @@ -46,6 +46,23 @@ antlrcpp::Any CypherMainVisitor::visitProfileQuery( return profile_query; } +antlrcpp::Any CypherMainVisitor::visitInfoQuery( + MemgraphCypher::InfoQueryContext *ctx) { + CHECK(ctx->children.size() == 2) + << "ProfileQuery should have exactly two children!"; + auto *info_query = storage_->Create(); + query_ = info_query; + if (ctx->storageInfo()) { + info_query->info_type_ = InfoQuery::InfoType::STORAGE; + return info_query; + } else if (ctx->indexInfo()) { + info_query->info_type_ = InfoQuery::InfoType::INDEX; + return info_query; + } else { + throw utils::NotYetImplemented("Info query: '{}'", ctx->getText()); + } +} + antlrcpp::Any CypherMainVisitor::visitCypherQuery( MemgraphCypher::CypherQueryContext *ctx) { auto *cypher_query = storage_->Create(); @@ -475,6 +492,7 @@ antlrcpp::Any CypherMainVisitor::visitPrivilege( if (ctx->SET()) return AuthQuery::Privilege::SET; if (ctx->REMOVE()) return AuthQuery::Privilege::REMOVE; if (ctx->INDEX()) return AuthQuery::Privilege::INDEX; + if (ctx->STATS()) return AuthQuery::Privilege::STATS; if (ctx->AUTH()) return AuthQuery::Privilege::AUTH; if (ctx->STREAM()) return AuthQuery::Privilege::STREAM; 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 c17396288..5e31d7ebe 100644 --- a/src/query/frontend/ast/cypher_main_visitor.hpp +++ b/src/query/frontend/ast/cypher_main_visitor.hpp @@ -159,6 +159,11 @@ class CypherMainVisitor : public antlropencypher::MemgraphCypherBaseVisitor { antlrcpp::Any visitProfileQuery( MemgraphCypher::ProfileQueryContext *ctx) override; + /** + * @return InfoQuery* + */ + antlrcpp::Any visitInfoQuery(MemgraphCypher::InfoQueryContext *ctx) override; + /** * @return AuthQuery* */ diff --git a/src/query/frontend/opencypher/grammar/Cypher.g4 b/src/query/frontend/opencypher/grammar/Cypher.g4 index a15536950..0420c2cc6 100644 --- a/src/query/frontend/opencypher/grammar/Cypher.g4 +++ b/src/query/frontend/opencypher/grammar/Cypher.g4 @@ -27,8 +27,15 @@ query : cypherQuery | indexQuery | explainQuery | profileQuery + | infoQuery ; +storageInfo : STORAGE INFO ; + +indexInfo : INDEX INFO ; + +infoQuery : SHOW ( storageInfo | indexInfo ) ; + explainQuery : EXPLAIN cypherQuery ; profileQuery : PROFILE cypherQuery ; diff --git a/src/query/frontend/opencypher/grammar/CypherLexer.g4 b/src/query/frontend/opencypher/grammar/CypherLexer.g4 index 08eaeb5fa..21ccd4a64 100644 --- a/src/query/frontend/opencypher/grammar/CypherLexer.g4 +++ b/src/query/frontend/opencypher/grammar/CypherLexer.g4 @@ -97,6 +97,7 @@ FALSE : F A L S E ; FILTER : F I L T E R ; IN : I N ; INDEX : I N D E X ; +INFO : I N F O ; IS : I S ; LIMIT : L I M I T ; L_SKIP : S K I P ; @@ -116,6 +117,7 @@ SET : S E T ; SHOW : S H O W ; SINGLE : S I N G L E ; STARTS : S T A R T S ; +STORAGE : S T O R A G E ; THEN : T H E N ; TRUE : T R U E ; UNION : U N I O N ; diff --git a/src/query/frontend/opencypher/grammar/MemgraphCypher.g4 b/src/query/frontend/opencypher/grammar/MemgraphCypher.g4 index 81d9b5623..fe1a3affb 100644 --- a/src/query/frontend/opencypher/grammar/MemgraphCypher.g4 +++ b/src/query/frontend/opencypher/grammar/MemgraphCypher.g4 @@ -49,6 +49,7 @@ query : cypherQuery | indexQuery | explainQuery | profileQuery + | infoQuery | authQuery | streamQuery ; @@ -98,7 +99,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 | AUTH | STREAM ; + | REMOVE | INDEX | STATS | AUTH | STREAM ; privilegeList : privilege ( ',' privilege )* ; diff --git a/src/query/frontend/opencypher/grammar/MemgraphCypherLexer.g4 b/src/query/frontend/opencypher/grammar/MemgraphCypherLexer.g4 index cbb97da34..b87668cd3 100644 --- a/src/query/frontend/opencypher/grammar/MemgraphCypherLexer.g4 +++ b/src/query/frontend/opencypher/grammar/MemgraphCypherLexer.g4 @@ -34,6 +34,7 @@ ROLE : R O L E ; ROLES : R O L E S ; SIZE : S I Z E ; START : S T A R T ; +STATS : S T A T S ; STOP : S T O P ; STREAM : S T R E A M ; STREAMS : S T R E A M S ; diff --git a/src/query/frontend/semantic/required_privileges.cpp b/src/query/frontend/semantic/required_privileges.cpp index 51bd9ab2c..5df081435 100644 --- a/src/query/frontend/semantic/required_privileges.cpp +++ b/src/query/frontend/semantic/required_privileges.cpp @@ -30,6 +30,19 @@ class PrivilegeExtractor : public QueryVisitor, query.cypher_query_->Accept(*this); } + void Visit(InfoQuery &info_query) override { + switch (info_query.info_type_) { + case InfoQuery::InfoType::INDEX: + // TODO: This should be INDEX | STATS, but we don't have support for + // *or* with privileges. + AddPrivilege(AuthQuery::Privilege::INDEX); + break; + case InfoQuery::InfoType::STORAGE: + AddPrivilege(AuthQuery::Privilege::STATS); + break; + } + } + 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 496c37231..7581b5bcc 100644 --- a/src/query/frontend/stripped_lexer_constants.hpp +++ b/src/query/frontend/stripped_lexer_constants.hpp @@ -89,8 +89,9 @@ const trie::Trie kKeywords = { "extract", "any", "none", "single", "true", "false", "reduce", "coalesce", "user", "password", "alter", "drop", "stream", "streams", "load", "data", "kafka", "transform", - "batch", "interval", "show", "start", "stop", "size", - "topic", "test", "unique", "explain", "profile"}; + "batch", "interval", "show", "start", "stats", "stop", + "size", "topic", "test", "unique", "explain", "profile", + "storage", "index", "info"}; // Unicode codepoints that are allowed at the start of the unescaped name. const std::bitset kUnescapedNameAllowedStarts(std::string( diff --git a/src/query/interpret/awesome_memgraph_functions.cpp b/src/query/interpret/awesome_memgraph_functions.cpp index 1995fa627..a14205201 100644 --- a/src/query/interpret/awesome_memgraph_functions.cpp +++ b/src/query/interpret/awesome_memgraph_functions.cpp @@ -675,15 +675,6 @@ TypedValue CounterSet(TypedValue *args, int64_t nargs, return TypedValue::Null; } -TypedValue IndexInfo(TypedValue *, int64_t nargs, const EvaluationContext &, - database::GraphDbAccessor *dba) { - if (nargs != 0) - throw QueryRuntimeException("'indexInfo' requires no arguments."); - - auto info = dba->IndexInfo(); - return std::vector(info.begin(), info.end()); -} - #ifdef MG_DISTRIBUTED TypedValue WorkerId(TypedValue *args, int64_t nargs, const EvaluationContext &, database::GraphDbAccessor *) { @@ -703,17 +694,6 @@ TypedValue WorkerId(TypedValue *args, int64_t nargs, const EvaluationContext &, } #endif -#if defined(MG_SINGLE_NODE) || defined(MG_SINGLE_NODE_HA) -TypedValue StorageInfo(TypedValue *, int64_t nargs, const EvaluationContext &, - database::GraphDbAccessor *dba) { - if (nargs != 0) - throw QueryRuntimeException("'storageInfo' requires no arguments."); - - auto info = dba->StorageInfo(); - return std::map(info.begin(), info.end()); -} -#endif - TypedValue Id(TypedValue *args, int64_t nargs, const EvaluationContext &, database::GraphDbAccessor *dba) { if (nargs != 1) { @@ -992,13 +972,9 @@ NameToFunction(const std::string &function_name) { if (function_name == "ASSERT") return Assert; if (function_name == "COUNTER") return Counter; if (function_name == "COUNTERSET") return CounterSet; - if (function_name == "INDEXINFO") return IndexInfo; #ifdef MG_DISTRIBUTED if (function_name == "WORKERID") return WorkerId; #endif -#if defined(MG_SINGLE_NODE) || defined(MG_SINGLE_NODE_HA) - if (function_name == "STORAGEINFO") return StorageInfo; -#endif return nullptr; } diff --git a/src/query/interpreter.cpp b/src/query/interpreter.cpp index 523fbd4c4..d7d228582 100644 --- a/src/query/interpreter.cpp +++ b/src/query/interpreter.cpp @@ -582,6 +582,41 @@ Callback HandleIndexQuery(IndexQuery *index_query, } } +Callback HandleInfoQuery(InfoQuery *info_query, database::GraphDbAccessor *db_accessor) { + Callback callback; + switch (info_query->info_type_) { + case InfoQuery::InfoType::STORAGE: +#if defined(MG_SINGLE_NODE) || defined(MG_SINGLE_NODE_HA) + callback.header = {"storage info", "value"}; + callback.fn = [db_accessor] { + auto info = db_accessor->StorageInfo(); + std::vector> results; + results.reserve(info.size()); + for (const auto &kv : info) { + results.push_back({kv.first, kv.second}); + } + return results; + }; +#else + throw utils::NotYetImplemented("storage info"); +#endif + break; + case InfoQuery::InfoType::INDEX: + callback.header = {"created index"}; + callback.fn = [db_accessor] { + auto info = db_accessor->IndexInfo(); + std::vector> results; + results.reserve(info.size()); + for (const auto &index : info) { + results.push_back({index}); + } + return results; + }; + break; + } + return callback; +} + Interpreter::Interpreter() : is_tsc_available_(utils::CheckAvailableTSC()) {} Interpreter::Results Interpreter::operator()( @@ -814,6 +849,8 @@ Interpreter::Results Interpreter::operator()( } callback = HandleStreamQuery(stream_query, kafka_streams_, parameters, &db_accessor); + } else if (auto *info_query = utils::Downcast(parsed_query.query)) { + callback = HandleInfoQuery(info_query, &db_accessor); } else { LOG(FATAL) << "Should not get here -- unknown query type!"; } diff --git a/tests/unit/ast_serialization.cpp b/tests/unit/ast_serialization.cpp index d453bd454..94a89ba2b 100644 --- a/tests/unit/ast_serialization.cpp +++ b/tests/unit/ast_serialization.cpp @@ -2109,12 +2109,7 @@ TYPED_TEST(CypherMainVisitorTest, RevokePrivilege) { {AuthQuery::Privilege::MATCH, AuthQuery::Privilege::AUTH}); check_auth_query( "REVOKE ALL PRIVILEGES FROM user", AuthQuery::Action::REVOKE_PRIVILEGE, - "", "", "user", {}, - {AuthQuery::Privilege::CREATE, AuthQuery::Privilege::DELETE, - AuthQuery::Privilege::MATCH, AuthQuery::Privilege::MERGE, - AuthQuery::Privilege::SET, AuthQuery::Privilege::REMOVE, - AuthQuery::Privilege::INDEX, AuthQuery::Privilege::AUTH, - AuthQuery::Privilege::STREAM}); + "", "", "user", {}, kPrivilegesAll); } TYPED_TEST(CypherMainVisitorTest, ShowPrivileges) { diff --git a/tests/unit/cypher_main_visitor.cpp b/tests/unit/cypher_main_visitor.cpp index e9e048805..f46b388e0 100644 --- a/tests/unit/cypher_main_visitor.cpp +++ b/tests/unit/cypher_main_visitor.cpp @@ -2167,12 +2167,7 @@ TYPED_TEST(CypherMainVisitorTest, RevokePrivilege) { {AuthQuery::Privilege::MATCH, AuthQuery::Privilege::AUTH}); check_auth_query( "REVOKE ALL PRIVILEGES FROM user", AuthQuery::Action::REVOKE_PRIVILEGE, - "", "", "user", {}, - {AuthQuery::Privilege::CREATE, AuthQuery::Privilege::DELETE, - AuthQuery::Privilege::MATCH, AuthQuery::Privilege::MERGE, - AuthQuery::Privilege::SET, AuthQuery::Privilege::REMOVE, - AuthQuery::Privilege::INDEX, AuthQuery::Privilege::AUTH, - AuthQuery::Privilege::STREAM}); + "", "", "user", {}, kPrivilegesAll); } TYPED_TEST(CypherMainVisitorTest, ShowPrivileges) { @@ -2495,4 +2490,22 @@ TYPED_TEST(CypherMainVisitorTest, TestProfileStreamQuery) { SyntaxException); } +TYPED_TEST(CypherMainVisitorTest, TestShowStorageInfo) { + { + TypeParam ast_generator("SHOW STORAGE INFO"); + auto *query = dynamic_cast(ast_generator.query_); + ASSERT_TRUE(query); + EXPECT_EQ(query->info_type_, InfoQuery::InfoType::STORAGE); + } +} + +TYPED_TEST(CypherMainVisitorTest, TestShowIndexInfo) { + { + TypeParam ast_generator("SHOW INDEX INFO"); + auto *query = dynamic_cast(ast_generator.query_); + ASSERT_TRUE(query); + EXPECT_EQ(query->info_type_, InfoQuery::InfoType::INDEX); + } +} + } // namespace diff --git a/tests/unit/query_expression_evaluator.cpp b/tests/unit/query_expression_evaluator.cpp index e3bcbe0d0..ed077affc 100644 --- a/tests/unit/query_expression_evaluator.cpp +++ b/tests/unit/query_expression_evaluator.cpp @@ -1438,30 +1438,6 @@ TEST_F(FunctionTest, CounterSet) { EXPECT_EQ(EvaluateFunction("COUNTER", {"c2"}).ValueInt(), 43); } -TEST_F(FunctionTest, IndexInfo) { - EXPECT_THROW(EvaluateFunction("INDEXINFO", {1}), QueryRuntimeException); - EXPECT_EQ(EvaluateFunction("INDEXINFO", {}).ValueList().size(), 0); - dba->InsertVertex().add_label(dba->Label("l1")); - { - auto info = ToList(EvaluateFunction("INDEXINFO", {})); - EXPECT_EQ(info.size(), 1); - EXPECT_EQ(info[0], ":l1"); - } - { - dba->BuildIndex(dba->Label("l1"), dba->Property("prop"), false); - auto info = ToList(EvaluateFunction("INDEXINFO", {})); - EXPECT_EQ(info.size(), 2); - EXPECT_THAT(info, testing::UnorderedElementsAre(":l1", ":l1(prop)")); - } - { - dba->BuildIndex(dba->Label("l1"), dba->Property("prop1"), true); - auto info = ToList(EvaluateFunction("INDEXINFO", {})); - EXPECT_EQ(info.size(), 3); - EXPECT_THAT(info, testing::UnorderedElementsAre(":l1", ":l1(prop)", - ":l1(prop1) unique")); - } -} - TEST_F(FunctionTest, Id) { auto va = dba->InsertVertex(); auto ea = dba->InsertEdge(va, va, dba->EdgeType("edge")); diff --git a/tests/unit/query_required_privileges.cpp b/tests/unit/query_required_privileges.cpp index 54ce27703..13387d820 100644 --- a/tests/unit/query_required_privileges.cpp +++ b/tests/unit/query_required_privileges.cpp @@ -132,3 +132,18 @@ TEST_F(TestPrivilegeExtractor, StreamQuery) { UnorderedElementsAre(AuthQuery::Privilege::STREAM)); } } + +TEST_F(TestPrivilegeExtractor, ShowIndexInfo) { + auto *query = storage.Create(); + query->info_type_ = InfoQuery::InfoType::INDEX; + EXPECT_THAT(GetRequiredPrivileges(query), + UnorderedElementsAre(AuthQuery::Privilege::INDEX)); +} + +TEST_F(TestPrivilegeExtractor, ShowStatsInfo) { + auto *query = storage.Create(); + query->info_type_ = InfoQuery::InfoType::STORAGE; + EXPECT_THAT(GetRequiredPrivileges(query), + UnorderedElementsAre(AuthQuery::Privilege::STATS)); +} +