diff --git a/src/auth/models.cpp b/src/auth/models.cpp index 54bf24da6..32b2ca291 100644 --- a/src/auth/models.cpp +++ b/src/auth/models.cpp @@ -37,7 +37,7 @@ const std::vector<Permission> kPermissionsAll = { Permission::CONSTRAINT, Permission::DUMP, Permission::AUTH, Permission::REPLICATION, Permission::DURABILITY, Permission::READ_FILE, Permission::FREE_MEMORY, Permission::TRIGGER, Permission::CONFIG, Permission::STREAM, Permission::MODULE_READ, Permission::MODULE_WRITE, - Permission::WEBSOCKET}; + Permission::WEBSOCKET, Permission::SCHEMA}; } // namespace std::string PermissionToString(Permission permission) { @@ -84,6 +84,8 @@ std::string PermissionToString(Permission permission) { return "MODULE_WRITE"; case Permission::WEBSOCKET: return "WEBSOCKET"; + case Permission::SCHEMA: + return "SCHEMA"; } } diff --git a/src/auth/models.hpp b/src/auth/models.hpp index 0f01c0a39..00c26464b 100644 --- a/src/auth/models.hpp +++ b/src/auth/models.hpp @@ -38,7 +38,8 @@ enum class Permission : uint64_t { STREAM = 1U << 17U, MODULE_READ = 1U << 18U, MODULE_WRITE = 1U << 19U, - WEBSOCKET = 1U << 20U + WEBSOCKET = 1U << 20U, + SCHEMA = 1U << 21U }; // clang-format on diff --git a/src/common/types.hpp b/src/common/types.hpp new file mode 100644 index 000000000..09a0aecf5 --- /dev/null +++ b/src/common/types.hpp @@ -0,0 +1,19 @@ +// Copyright 2022 Memgraph Ltd. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source +// License, and you may not use this file except in compliance with the Business Source License. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +#pragma once + +#include <cstdint> + +namespace memgraph::common { +enum class SchemaType : uint8_t { BOOL, INT, STRING, DATE, LOCALTIME, LOCALDATETIME, DURATION }; + +} // namespace memgraph::common diff --git a/src/glue/auth.cpp b/src/glue/auth.cpp index 7f05d8045..5d9ffbb84 100644 --- a/src/glue/auth.cpp +++ b/src/glue/auth.cpp @@ -57,6 +57,8 @@ auth::Permission PrivilegeToPermission(query::AuthQuery::Privilege privilege) { return auth::Permission::MODULE_WRITE; case query::AuthQuery::Privilege::WEBSOCKET: return auth::Permission::WEBSOCKET; + case query::AuthQuery::Privilege::SCHEMA: + return auth::Permission::SCHEMA; } } } // namespace memgraph::glue diff --git a/src/query/frontend/ast/ast.lcp b/src/query/frontend/ast/ast.lcp index 33d397754..de24ae37a 100644 --- a/src/query/frontend/ast/ast.lcp +++ b/src/query/frontend/ast/ast.lcp @@ -17,6 +17,7 @@ #include <variant> #include <vector> +#include "common/types.hpp" #include "query/frontend/ast/ast_visitor.hpp" #include "query/frontend/semantic/symbol.hpp" #include "query/interpret/awesome_memgraph_functions.hpp" @@ -133,6 +134,15 @@ cpp<# } cpp<#)) + +(defun clone-schema-property-vector (source dest) + #>cpp + ${dest}.reserve(${source}.size()); + for (const auto &[property_ix, property_type]: ${source}) { + ${dest}.emplace_back(storage->GetPropertyIx(property_ix.name), property_type); + } + cpp<#) + ;; The following index structs serve as a decoupling point of AST from ;; concrete database types. All the names are collected in AstStorage, and can ;; be indexed through these instances. This means that we can create a vector @@ -2253,7 +2263,7 @@ cpp<# (lcp:define-enum privilege (create delete match merge set remove index stats auth constraint dump replication durability read_file free_memory trigger config stream module_read module_write - websocket) + websocket schema) (:serialize)) #>cpp AuthQuery() = default; @@ -2295,7 +2305,7 @@ const std::vector<AuthQuery::Privilege> kPrivilegesAll = { AuthQuery::Privilege::FREE_MEMORY, AuthQuery::Privilege::TRIGGER, AuthQuery::Privilege::CONFIG, AuthQuery::Privilege::STREAM, AuthQuery::Privilege::MODULE_READ, AuthQuery::Privilege::MODULE_WRITE, - AuthQuery::Privilege::WEBSOCKET}; + AuthQuery::Privilege::WEBSOCKET, AuthQuery::Privilege::SCHEMA}; cpp<# (lcp:define-class info-query (query) @@ -2668,5 +2678,38 @@ cpp<# (:serialize (:slk)) (:clone)) +(lcp:define-class schema-query (query) + ((action "Action" :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<#)) + (schema_type_map "std::vector<std::pair<PropertyIx, common::SchemaType>>" + :slk-save #'slk-save-property-map + :slk-load #'slk-load-property-map + :clone #'clone-schema-property-vector + :scope :public)) + + (:public + (lcp:define-enum action + (create-schema drop-schema show-schema show-schemas) + (:serialize)) + #>cpp + SchemaQuery() = default; + + DEFVISITABLE(QueryVisitor<void>); + cpp<#) + (:private + #>cpp + friend class AstStorage; + cpp<#) + (:serialize (:slk)) + (:clone)) + (lcp:pop-namespace) ;; namespace query (lcp:pop-namespace) ;; namespace memgraph diff --git a/src/query/frontend/ast/ast_visitor.hpp b/src/query/frontend/ast/ast_visitor.hpp index 0e4a6012c..307b96907 100644 --- a/src/query/frontend/ast/ast_visitor.hpp +++ b/src/query/frontend/ast/ast_visitor.hpp @@ -94,6 +94,7 @@ class StreamQuery; class SettingQuery; class VersionQuery; class Foreach; +class SchemaQuery; using TreeCompositeVisitor = utils::CompositeVisitor< SingleQuery, CypherUnion, NamedExpression, OrOperator, XorOperator, AndOperator, NotOperator, AdditionOperator, @@ -125,9 +126,9 @@ class ExpressionVisitor None, ParameterLookup, Identifier, PrimitiveLiteral, RegexMatch> {}; template <class TResult> -class QueryVisitor - : public utils::Visitor<TResult, CypherQuery, ExplainQuery, ProfileQuery, IndexQuery, AuthQuery, InfoQuery, - ConstraintQuery, DumpQuery, ReplicationQuery, LockPathQuery, FreeMemoryQuery, TriggerQuery, - IsolationLevelQuery, CreateSnapshotQuery, StreamQuery, SettingQuery, VersionQuery> {}; +class QueryVisitor : public utils::Visitor<TResult, CypherQuery, ExplainQuery, ProfileQuery, IndexQuery, AuthQuery, + InfoQuery, ConstraintQuery, DumpQuery, ReplicationQuery, LockPathQuery, + FreeMemoryQuery, TriggerQuery, IsolationLevelQuery, CreateSnapshotQuery, + StreamQuery, SettingQuery, VersionQuery, SchemaQuery> {}; } // namespace memgraph::query diff --git a/src/query/frontend/ast/cypher_main_visitor.cpp b/src/query/frontend/ast/cypher_main_visitor.cpp index f4a269dfd..2e73b2fbb 100644 --- a/src/query/frontend/ast/cypher_main_visitor.cpp +++ b/src/query/frontend/ast/cypher_main_visitor.cpp @@ -17,6 +17,7 @@ #include <cstring> #include <iterator> #include <limits> +#include <ranges> #include <string> #include <tuple> #include <type_traits> @@ -27,6 +28,7 @@ #include <boost/preprocessor/cat.hpp> +#include "common/types.hpp" #include "query/exceptions.hpp" #include "query/frontend/ast/ast.hpp" #include "query/frontend/ast/ast_visitor.hpp" @@ -1355,6 +1357,7 @@ antlrcpp::Any CypherMainVisitor::visitPrivilege(MemgraphCypher::PrivilegeContext if (ctx->MODULE_READ()) return AuthQuery::Privilege::MODULE_READ; if (ctx->MODULE_WRITE()) return AuthQuery::Privilege::MODULE_WRITE; if (ctx->WEBSOCKET()) return AuthQuery::Privilege::WEBSOCKET; + if (ctx->SCHEMA()) return AuthQuery::Privilege::SCHEMA; LOG_FATAL("Should not get here - unknown privilege!"); } @@ -2353,6 +2356,93 @@ antlrcpp::Any CypherMainVisitor::visitForeach(MemgraphCypher::ForeachContext *ct return for_each; } +antlrcpp::Any CypherMainVisitor::visitSchemaQuery(MemgraphCypher::SchemaQueryContext *ctx) { + MG_ASSERT(ctx->children.size() == 1, "SchemaQuery should have exactly one child!"); + auto *schema_query = ctx->children[0]->accept(this).as<SchemaQuery *>(); + query_ = schema_query; + return schema_query; +} + +antlrcpp::Any CypherMainVisitor::visitShowSchema(MemgraphCypher::ShowSchemaContext *ctx) { + auto *schema_query = storage_->Create<SchemaQuery>(); + schema_query->action_ = SchemaQuery::Action::SHOW_SCHEMA; + schema_query->label_ = AddLabel(ctx->labelName()->accept(this)); + query_ = schema_query; + return schema_query; +} + +antlrcpp::Any CypherMainVisitor::visitShowSchemas(MemgraphCypher::ShowSchemasContext * /*ctx*/) { + auto *schema_query = storage_->Create<SchemaQuery>(); + schema_query->action_ = SchemaQuery::Action::SHOW_SCHEMAS; + query_ = schema_query; + return schema_query; +} + +antlrcpp::Any CypherMainVisitor::visitPropertyType(MemgraphCypher::PropertyTypeContext *ctx) { + MG_ASSERT(ctx->symbolicName()); + const auto property_type = utils::ToLowerCase(ctx->symbolicName()->accept(this).as<std::string>()); + if (property_type == "bool") { + return common::SchemaType::BOOL; + } + if (property_type == "string") { + return common::SchemaType::STRING; + } + if (property_type == "integer") { + return common::SchemaType::INT; + } + if (property_type == "date") { + return common::SchemaType::DATE; + } + if (property_type == "duration") { + return common::SchemaType::DURATION; + } + if (property_type == "localdatetime") { + return common::SchemaType::LOCALDATETIME; + } + if (property_type == "localtime") { + return common::SchemaType::LOCALTIME; + } + throw SyntaxException("Property type must be one of the supported types!"); +} + +/** + * @return Schema* + */ +antlrcpp::Any CypherMainVisitor::visitSchemaPropertyMap(MemgraphCypher::SchemaPropertyMapContext *ctx) { + std::vector<std::pair<PropertyIx, common::SchemaType>> schema_property_map; + for (auto *property_key_pair : ctx->propertyKeyTypePair()) { + PropertyIx key = property_key_pair->propertyKeyName()->accept(this); + common::SchemaType type = property_key_pair->propertyType()->accept(this); + if (std::ranges::find_if(schema_property_map, [&key](const auto &elem) { return elem.first == key; }) != + schema_property_map.end()) { + throw SemanticException("Same property name can't appear twice in a schema map."); + } + schema_property_map.emplace_back(key, type); + } + return schema_property_map; +} + +antlrcpp::Any CypherMainVisitor::visitCreateSchema(MemgraphCypher::CreateSchemaContext *ctx) { + auto *schema_query = storage_->Create<SchemaQuery>(); + schema_query->action_ = SchemaQuery::Action::CREATE_SCHEMA; + schema_query->label_ = AddLabel(ctx->labelName()->accept(this)); + schema_query->schema_type_map_ = + ctx->schemaPropertyMap()->accept(this).as<std::vector<std::pair<PropertyIx, common::SchemaType>>>(); + query_ = schema_query; + return schema_query; +} + +/** + * @return Schema* + */ +antlrcpp::Any CypherMainVisitor::visitDropSchema(MemgraphCypher::DropSchemaContext *ctx) { + auto *schema_query = storage_->Create<SchemaQuery>(); + schema_query->action_ = SchemaQuery::Action::DROP_SCHEMA; + schema_query->label_ = AddLabel(ctx->labelName()->accept(this)); + query_ = schema_query; + return schema_query; +} + LabelIx CypherMainVisitor::AddLabel(const std::string &name) { return storage_->GetLabelIx(name); } PropertyIx CypherMainVisitor::AddProperty(const std::string &name) { return storage_->GetPropertyIx(name); } diff --git a/src/query/frontend/ast/cypher_main_visitor.hpp b/src/query/frontend/ast/cypher_main_visitor.hpp index 2a6b8ff5e..a4216e702 100644 --- a/src/query/frontend/ast/cypher_main_visitor.hpp +++ b/src/query/frontend/ast/cypher_main_visitor.hpp @@ -849,6 +849,41 @@ class CypherMainVisitor : public antlropencypher::MemgraphCypherBaseVisitor { */ antlrcpp::Any visitForeach(MemgraphCypher::ForeachContext *ctx) override; + /** + * @return Schema* + */ + antlrcpp::Any visitPropertyType(MemgraphCypher::PropertyTypeContext *ctx) override; + + /** + * @return Schema* + */ + antlrcpp::Any visitSchemaPropertyMap(MemgraphCypher::SchemaPropertyMapContext *ctx) override; + + /** + * @return Schema* + */ + antlrcpp::Any visitSchemaQuery(MemgraphCypher::SchemaQueryContext *ctx) override; + + /** + * @return Schema* + */ + antlrcpp::Any visitShowSchema(MemgraphCypher::ShowSchemaContext *ctx) override; + + /** + * @return Schema* + */ + antlrcpp::Any visitShowSchemas(MemgraphCypher::ShowSchemasContext *ctx) override; + + /** + * @return Schema* + */ + antlrcpp::Any visitCreateSchema(MemgraphCypher::CreateSchemaContext *ctx) override; + + /** + * @return Schema* + */ + antlrcpp::Any visitDropSchema(MemgraphCypher::DropSchemaContext *ctx) override; + public: Query *query() { return query_; } const static std::string kAnonPrefix; diff --git a/src/query/frontend/opencypher/grammar/MemgraphCypher.g4 b/src/query/frontend/opencypher/grammar/MemgraphCypher.g4 index b412a474a..df51de704 100644 --- a/src/query/frontend/opencypher/grammar/MemgraphCypher.g4 +++ b/src/query/frontend/opencypher/grammar/MemgraphCypher.g4 @@ -46,10 +46,10 @@ memgraphCypherKeyword : cypherKeyword | DROP | DUMP | EXECUTE - | FOR - | FOREACH | FREE | FROM + | FOR + | FOREACH | GLOBAL | GRANT | HEADER @@ -76,6 +76,8 @@ memgraphCypherKeyword : cypherKeyword | ROLE | ROLES | QUOTE + | SCHEMA + | SCHEMAS | SESSION | SETTING | SETTINGS @@ -122,6 +124,7 @@ query : cypherQuery | streamQuery | settingQuery | versionQuery + | schemaQuery ; authQuery : createRole @@ -192,6 +195,12 @@ settingQuery : setSetting | showSettings ; +schemaQuery : showSchema + | showSchemas + | createSchema + | dropSchema + ; + loadCsv : LOAD CSV FROM csvFile ( WITH | NO ) HEADER ( IGNORE BAD ) ? ( DELIMITER delimiter ) ? @@ -254,6 +263,7 @@ privilege : CREATE | MODULE_READ | MODULE_WRITE | WEBSOCKET + | SCHEMA ; privilegeList : privilege ( ',' privilege )* ; @@ -374,3 +384,17 @@ showSetting : SHOW DATABASE SETTING settingName ; showSettings : SHOW DATABASE SETTINGS ; versionQuery : SHOW VERSION ; + +showSchema : SHOW SCHEMA ON ':' labelName ; + +showSchemas : SHOW SCHEMAS ; + +propertyType : symbolicName ; + +propertyKeyTypePair : propertyKeyName propertyType ; + +schemaPropertyMap : '(' propertyKeyTypePair ( ',' propertyKeyTypePair )* ')' ; + +createSchema : CREATE SCHEMA ON ':' labelName schemaPropertyMap ; + +dropSchema : DROP SCHEMA ON ':' labelName ; diff --git a/src/query/frontend/opencypher/grammar/MemgraphCypherLexer.g4 b/src/query/frontend/opencypher/grammar/MemgraphCypherLexer.g4 index 55e5d53a2..869141033 100644 --- a/src/query/frontend/opencypher/grammar/MemgraphCypherLexer.g4 +++ b/src/query/frontend/opencypher/grammar/MemgraphCypherLexer.g4 @@ -89,6 +89,8 @@ REVOKE : R E V O K E ; ROLE : R O L E ; ROLES : R O L E S ; QUOTE : Q U O T E ; +SCHEMA : S C H E M A ; +SCHEMAS : S C H E M A S ; SERVICE_URL : S E R V I C E UNDERSCORE U R L ; SESSION : S E S S I O N ; SETTING : S E T T I N G ; diff --git a/src/query/frontend/semantic/required_privileges.cpp b/src/query/frontend/semantic/required_privileges.cpp index e8dbd21e5..160004ac2 100644 --- a/src/query/frontend/semantic/required_privileges.cpp +++ b/src/query/frontend/semantic/required_privileges.cpp @@ -80,6 +80,8 @@ class PrivilegeExtractor : public QueryVisitor<void>, public HierarchicalTreeVis void Visit(VersionQuery & /*version_query*/) override { AddPrivilege(AuthQuery::Privilege::STATS); } + void Visit(SchemaQuery & /*schema_query*/) override { AddPrivilege(AuthQuery::Privilege::SCHEMA); } + bool PreVisit(Create & /*unused*/) override { AddPrivilege(AuthQuery::Privilege::CREATE); return false; diff --git a/src/query/frontend/stripped_lexer_constants.hpp b/src/query/frontend/stripped_lexer_constants.hpp index 42b7b4aeb..be516aee6 100644 --- a/src/query/frontend/stripped_lexer_constants.hpp +++ b/src/query/frontend/stripped_lexer_constants.hpp @@ -204,8 +204,9 @@ const trie::Trie kKeywords = {"union", "pulsar", "service_url", "version", - "websocket" - "foreach"}; + "websocket", + "foreach", + "schema"}; // Unicode codepoints that are allowed at the start of the unescaped name. const std::bitset<kBitsetSize> kUnescapedNameAllowedStarts( diff --git a/src/query/interpreter.cpp b/src/query/interpreter.cpp index a8bb42dc9..fbb1cbc55 100644 --- a/src/query/interpreter.cpp +++ b/src/query/interpreter.cpp @@ -44,6 +44,7 @@ #include "query/trigger.hpp" #include "query/typed_value.hpp" #include "storage/v2/property_value.hpp" +#include "storage/v2/schemas.hpp" #include "utils/algorithm.hpp" #include "utils/csv_parsing.hpp" #include "utils/event_counter.hpp" @@ -891,6 +892,102 @@ Callback HandleSettingQuery(SettingQuery *setting_query, const Parameters ¶m } } +Callback HandleSchemaQuery(SchemaQuery *schema_query, InterpreterContext *interpreter_context, + std::vector<Notification> *notifications) { + Callback callback; + switch (schema_query->action_) { + case SchemaQuery::Action::SHOW_SCHEMAS: { + callback.header = {"label", "primary_key"}; + callback.fn = [interpreter_context]() { + auto *db = interpreter_context->db; + auto schemas_info = db->ListAllSchemas(); + std::vector<std::vector<TypedValue>> results; + results.reserve(schemas_info.schemas.size()); + + for (const auto &[label_id, schema_types] : schemas_info.schemas) { + std::vector<TypedValue> schema_info_row; + schema_info_row.reserve(3); + + schema_info_row.emplace_back(db->LabelToName(label_id)); + std::vector<std::string> primary_key_properties; + primary_key_properties.reserve(schema_types.size()); + std::transform(schema_types.begin(), schema_types.end(), std::back_inserter(primary_key_properties), + [&db](const auto &schema_type) { + return db->PropertyToName(schema_type.property_id) + + "::" + storage::SchemaTypeToString(schema_type.type); + }); + + schema_info_row.emplace_back(utils::Join(primary_key_properties, ", ")); + results.push_back(std::move(schema_info_row)); + } + return results; + }; + return callback; + } + case SchemaQuery::Action::SHOW_SCHEMA: { + callback.header = {"property_name", "property_type"}; + callback.fn = [interpreter_context, primary_label = schema_query->label_]() { + auto *db = interpreter_context->db; + const auto label = db->NameToLabel(primary_label.name); + const auto schema = db->GetSchema(label); + std::vector<std::vector<TypedValue>> results; + if (schema) { + for (const auto &schema_property : schema->second) { + std::vector<TypedValue> schema_info_row; + schema_info_row.reserve(2); + schema_info_row.emplace_back(db->PropertyToName(schema_property.property_id)); + schema_info_row.emplace_back(storage::SchemaTypeToString(schema_property.type)); + results.push_back(std::move(schema_info_row)); + } + return results; + } + throw QueryException(fmt::format("Schema on label :{} not found!", primary_label.name)); + }; + return callback; + } + case SchemaQuery::Action::CREATE_SCHEMA: { + auto schema_type_map = schema_query->schema_type_map_; + if (schema_query->schema_type_map_.empty()) { + throw SyntaxException("One or more types have to be defined in schema definition."); + } + callback.fn = [interpreter_context, primary_label = schema_query->label_, + schema_type_map = std::move(schema_type_map)]() { + auto *db = interpreter_context->db; + const auto label = db->NameToLabel(primary_label.name); + std::vector<storage::SchemaProperty> schemas_types; + schemas_types.reserve(schema_type_map.size()); + for (const auto &schema_type : schema_type_map) { + auto property_id = db->NameToProperty(schema_type.first.name); + schemas_types.push_back({property_id, schema_type.second}); + } + if (!db->CreateSchema(label, schemas_types)) { + throw QueryException(fmt::format("Schema on label :{} already exists!", primary_label.name)); + } + return std::vector<std::vector<TypedValue>>{}; + }; + notifications->emplace_back(SeverityLevel::INFO, NotificationCode::CREATE_SCHEMA, + fmt::format("Create schema on label :{}", schema_query->label_.name)); + return callback; + } + case SchemaQuery::Action::DROP_SCHEMA: { + callback.fn = [interpreter_context, primary_label = schema_query->label_]() { + auto *db = interpreter_context->db; + const auto label = db->NameToLabel(primary_label.name); + + if (!db->DropSchema(label)) { + throw QueryException(fmt::format("Schema on label :{} does not exist!", primary_label.name)); + } + + return std::vector<std::vector<TypedValue>>{}; + }; + notifications->emplace_back(SeverityLevel::INFO, NotificationCode::DROP_SCHEMA, + fmt::format("Dropped schema on label :{}", schema_query->label_.name)); + return callback; + } + } + return callback; +} + // Struct for lazy pulling from a vector struct PullPlanVector { explicit PullPlanVector(std::vector<std::vector<TypedValue>> values) : values_(std::move(values)) {} @@ -2086,6 +2183,32 @@ PreparedQuery PrepareConstraintQuery(ParsedQuery parsed_query, bool in_explicit_ RWType::NONE}; } +PreparedQuery PrepareSchemaQuery(ParsedQuery parsed_query, bool in_explicit_transaction, + InterpreterContext *interpreter_context, std::vector<Notification> *notifications) { + if (in_explicit_transaction) { + throw ConstraintInMulticommandTxException(); + } + auto *schema_query = utils::Downcast<SchemaQuery>(parsed_query.query); + MG_ASSERT(schema_query); + auto callback = HandleSchemaQuery(schema_query, interpreter_context, notifications); + + return PreparedQuery{std::move(callback.header), std::move(parsed_query.required_privileges), + [handler = std::move(callback.fn), action = QueryHandlerResult::NOTHING, + pull_plan = std::shared_ptr<PullPlanVector>(nullptr)]( + AnyStream *stream, std::optional<int> n) mutable -> std::optional<QueryHandlerResult> { + if (!pull_plan) { + auto results = handler(); + pull_plan = std::make_shared<PullPlanVector>(std::move(results)); + } + + if (pull_plan->Pull(stream, n)) { + return action; + } + return std::nullopt; + }, + RWType::NONE}; +} + void Interpreter::BeginTransaction() { const auto prepared_query = PrepareTransactionQuery("BEGIN"); prepared_query.query_handler(nullptr, {}); @@ -2219,6 +2342,9 @@ Interpreter::PrepareResult Interpreter::Prepare(const std::string &query_string, prepared_query = PrepareSettingQuery(std::move(parsed_query), in_explicit_transaction_, &*execution_db_accessor_); } else if (utils::Downcast<VersionQuery>(parsed_query.query)) { prepared_query = PrepareVersionQuery(std::move(parsed_query), in_explicit_transaction_); + } else if (utils::Downcast<SchemaQuery>(parsed_query.query)) { + prepared_query = PrepareSchemaQuery(std::move(parsed_query), in_explicit_transaction_, interpreter_context_, + &query_execution->notifications); } else { LOG_FATAL("Should not get here -- unknown query type!"); } diff --git a/src/query/metadata.cpp b/src/query/metadata.cpp index f4e8512fd..2e25ce8a4 100644 --- a/src/query/metadata.cpp +++ b/src/query/metadata.cpp @@ -38,6 +38,8 @@ constexpr std::string_view GetCodeString(const NotificationCode code) { return "CreateIndex"sv; case NotificationCode::CREATE_STREAM: return "CreateStream"sv; + case NotificationCode::CREATE_SCHEMA: + return "CreateSchema"sv; case NotificationCode::CHECK_STREAM: return "CheckStream"sv; case NotificationCode::CREATE_TRIGGER: @@ -48,6 +50,8 @@ constexpr std::string_view GetCodeString(const NotificationCode code) { return "DropReplica"sv; case NotificationCode::DROP_INDEX: return "DropIndex"sv; + case NotificationCode::DROP_SCHEMA: + return "DropSchema"sv; case NotificationCode::DROP_STREAM: return "DropStream"sv; case NotificationCode::DROP_TRIGGER: @@ -68,6 +72,10 @@ constexpr std::string_view GetCodeString(const NotificationCode code) { return "ReplicaPortWarning"sv; case NotificationCode::SET_REPLICA: return "SetReplica"sv; + case NotificationCode::SHOW_SCHEMA: + return "ShowSchema"sv; + case NotificationCode::SHOW_SCHEMAS: + return "ShowSchemas"sv; case NotificationCode::START_STREAM: return "StartStream"sv; case NotificationCode::START_ALL_STREAMS: @@ -114,4 +122,4 @@ std::string ExecutionStatsKeyToString(const ExecutionStats::Key key) { } } -} // namespace memgraph::query \ No newline at end of file +} // namespace memgraph::query diff --git a/src/query/metadata.hpp b/src/query/metadata.hpp index 67f784fa8..e557ca72e 100644 --- a/src/query/metadata.hpp +++ b/src/query/metadata.hpp @@ -26,12 +26,14 @@ enum class SeverityLevel : uint8_t { INFO, WARNING }; enum class NotificationCode : uint8_t { CREATE_CONSTRAINT, CREATE_INDEX, + CREATE_SCHEMA, CHECK_STREAM, CREATE_STREAM, CREATE_TRIGGER, DROP_CONSTRAINT, DROP_INDEX, DROP_REPLICA, + DROP_SCHEMA, DROP_STREAM, DROP_TRIGGER, EXISTANT_INDEX, @@ -42,6 +44,8 @@ enum class NotificationCode : uint8_t { REPLICA_PORT_WARNING, REGISTER_REPLICA, SET_REPLICA, + SHOW_SCHEMA, + SHOW_SCHEMAS, START_STREAM, START_ALL_STREAMS, STOP_STREAM, diff --git a/src/storage/v2/schemas.cpp b/src/storage/v2/schemas.cpp index 2e4fbbe3b..1bec8455a 100644 --- a/src/storage/v2/schemas.cpp +++ b/src/storage/v2/schemas.cpp @@ -26,14 +26,31 @@ SchemaViolation::SchemaViolation(ValidationStatus status, LabelId label, SchemaP PropertyValue violated_property_value) : status{status}, label{label}, violated_type{violated_type}, violated_property_value{violated_property_value} {} -bool Schemas::CreateSchema(const LabelId primary_label, const std::vector<SchemaProperty> &schemas_types) { - return schemas_.insert({primary_label, schemas_types}).second; +Schemas::SchemasList Schemas::ListSchemas() const { + Schemas::SchemasList ret; + ret.reserve(schemas_.size()); + std::transform(schemas_.begin(), schemas_.end(), std::back_inserter(ret), + [](const auto &schema_property_type) { return schema_property_type; }); + return ret; } -bool Schemas::DeleteSchema(const LabelId primary_label) { - return schemas_.erase(primary_label); +std::optional<Schemas::Schema> Schemas::GetSchema(const LabelId primary_label) const { + if (auto schema_map = schemas_.find(primary_label); schema_map != schemas_.end()) { + return Schema{schema_map->first, schema_map->second}; + } + return std::nullopt; } +bool Schemas::CreateSchema(const LabelId primary_label, const std::vector<SchemaProperty> &schemas_types) { + if (schemas_.contains(primary_label)) { + return false; + } + schemas_.emplace(primary_label, schemas_types); + return true; +} + +bool Schemas::DropSchema(const LabelId primary_label) { return schemas_.erase(primary_label); } + std::optional<SchemaViolation> Schemas::ValidateVertex(const LabelId primary_label, const Vertex &vertex) { // TODO Check for multiple defined primary labels const auto schema = schemas_.find(primary_label); @@ -51,7 +68,7 @@ std::optional<SchemaViolation> Schemas::ValidateVertex(const LabelId primary_lab // Property type check // TODO Can this be replaced with just property id check? if (auto vertex_property = vertex.properties.GetProperty(schema_type.property_id); - PropertyValueTypeToSchemaProperty(vertex_property) != schema_type.type) { + PropertyTypeToSchemaType(vertex_property) != schema_type.type) { return SchemaViolation(SchemaViolation::ValidationStatus::VERTEX_PROPERTY_WRONG_TYPE, primary_label, schema_type, vertex_property); } @@ -62,13 +79,66 @@ std::optional<SchemaViolation> Schemas::ValidateVertex(const LabelId primary_lab return std::nullopt; } -Schemas::SchemasList Schemas::ListSchemas() const { - Schemas::SchemasList ret; - ret.reserve(schemas_.size()); - for (const auto &[label_props, schema_property] : schemas_) { - ret.emplace_back(label_props, schema_property); +std::optional<common::SchemaType> PropertyTypeToSchemaType(const PropertyValue &property_value) { + switch (property_value.type()) { + case PropertyValue::Type::Bool: { + return common::SchemaType::BOOL; + } + case PropertyValue::Type::Int: { + return common::SchemaType::INT; + } + case PropertyValue::Type::String: { + return common::SchemaType::STRING; + } + case PropertyValue::Type::TemporalData: { + switch (property_value.ValueTemporalData().type) { + case TemporalType::Date: { + return common::SchemaType::DATE; + } + case TemporalType::LocalDateTime: { + return common::SchemaType::LOCALDATETIME; + } + case TemporalType::LocalTime: { + return common::SchemaType::LOCALTIME; + } + case TemporalType::Duration: { + return common::SchemaType::DURATION; + } + } + } + case PropertyValue::Type::Double: + case PropertyValue::Type::Null: + case PropertyValue::Type::Map: + case PropertyValue::Type::List: { + return std::nullopt; + } + } +} + +std::string SchemaTypeToString(const common::SchemaType type) { + switch (type) { + case common::SchemaType::BOOL: { + return "Bool"; + } + case common::SchemaType::INT: { + return "Integer"; + } + case common::SchemaType::STRING: { + return "String"; + } + case common::SchemaType::DATE: { + return "Date"; + } + case common::SchemaType::LOCALTIME: { + return "LocalTime"; + } + case common::SchemaType::LOCALDATETIME: { + return "LocalDateTime"; + } + case common::SchemaType::DURATION: { + return "Duration"; + } } - return ret; } } // namespace memgraph::storage diff --git a/src/storage/v2/schemas.hpp b/src/storage/v2/schemas.hpp index 113707069..898288b6b 100644 --- a/src/storage/v2/schemas.hpp +++ b/src/storage/v2/schemas.hpp @@ -17,6 +17,7 @@ #include <utility> #include <vector> +#include "common/types.hpp" #include "storage/v2/id_types.hpp" #include "storage/v2/indices.hpp" #include "storage/v2/property_value.hpp" @@ -32,10 +33,8 @@ class SchemaViolationException : public utils::BasicException { }; struct SchemaProperty { - enum class Type : uint8_t { Bool, Int, Double, String, Date, LocalTime, LocalDateTime, Duration }; - - Type type; PropertyId property_id; + common::SchemaType type; }; struct SchemaViolation { @@ -63,8 +62,9 @@ struct SchemaViolation { /// Schema can be mapped under only one label => primary label class Schemas { public: + using Schema = std::pair<LabelId, std::vector<SchemaProperty>>; using SchemasMap = std::unordered_map<LabelId, std::vector<SchemaProperty>>; - using SchemasList = std::vector<std::pair<LabelId, std::vector<SchemaProperty>>>; + using SchemasList = std::vector<Schema>; Schemas() = default; Schemas(const Schemas &) = delete; @@ -73,83 +73,26 @@ class Schemas { Schemas &operator=(Schemas &&) = delete; ~Schemas() = default; + [[nodiscard]] SchemasList ListSchemas() const; + + [[nodiscard]] std::optional<Schemas::Schema> GetSchema(LabelId primary_label) const; + + // Returns true if it was successfully created or false if the schema + // already exists [[nodiscard]] bool CreateSchema(LabelId label, const std::vector<SchemaProperty> &schemas_types); - [[nodiscard]] bool DeleteSchema(LabelId label); + // Returns true if it was successfully dropped or false if the schema + // does not exist + [[nodiscard]] bool DropSchema(LabelId label); [[nodiscard]] std::optional<SchemaViolation> ValidateVertex(LabelId primary_label, const Vertex &vertex); - [[nodiscard]] SchemasList ListSchemas() const; - private: SchemasMap schemas_; }; -inline std::optional<SchemaProperty::Type> PropertyValueTypeToSchemaProperty(const PropertyValue &property_value) { - switch (property_value.type()) { - case PropertyValue::Type::Bool: { - return SchemaProperty::Type::Bool; - } - case PropertyValue::Type::Int: { - return SchemaProperty::Type::Int; - } - case PropertyValue::Type::Double: { - return SchemaProperty::Type::Double; - } - case PropertyValue::Type::String: { - return SchemaProperty::Type::String; - } - case PropertyValue::Type::TemporalData: { - switch (property_value.ValueTemporalData().type) { - case TemporalType::Date: { - return SchemaProperty::Type::Date; - } - case TemporalType::LocalDateTime: { - return SchemaProperty::Type::LocalDateTime; - } - case TemporalType::LocalTime: { - return SchemaProperty::Type::LocalTime; - } - case TemporalType::Duration: { - return SchemaProperty::Type::Duration; - } - } - } - case PropertyValue::Type::Null: - case PropertyValue::Type::Map: - case PropertyValue::Type::List: { - return std::nullopt; - } - } -} +std::optional<common::SchemaType> PropertyTypeToSchemaType(const PropertyValue &property_value); -inline std::string SchemaPropertyToString(const SchemaProperty::Type type) { - switch (type) { - case SchemaProperty::Type::Bool: { - return "Bool"; - } - case SchemaProperty::Type::Int: { - return "Integer"; - } - case SchemaProperty::Type::Double: { - return "Double"; - } - case SchemaProperty::Type::String: { - return "String"; - } - case SchemaProperty::Type::Date: { - return "Date"; - } - case SchemaProperty::Type::LocalTime: { - return "LocalTime"; - } - case SchemaProperty::Type::LocalDateTime: { - return "LocalDateTime"; - } - case SchemaProperty::Type::Duration: { - return "Duration"; - } - } -} +std::string SchemaTypeToString(common::SchemaType type); } // namespace memgraph::storage diff --git a/src/storage/v2/storage.cpp b/src/storage/v2/storage.cpp index 5b1f3084b..36f306a62 100644 --- a/src/storage/v2/storage.cpp +++ b/src/storage/v2/storage.cpp @@ -1241,6 +1241,17 @@ SchemasInfo Storage::ListAllSchemas() const { return {schemas_.ListSchemas()}; } +std::optional<Schemas::Schema> Storage::GetSchema(const LabelId primary_label) const { + std::shared_lock<utils::RWLock> storage_guard_(main_lock_); + return schemas_.GetSchema(primary_label); +} + +bool Storage::CreateSchema(const LabelId primary_label, const std::vector<SchemaProperty> &schemas_types) { + return schemas_.CreateSchema(primary_label, schemas_types); +} + +bool Storage::DropSchema(const LabelId primary_label) { return schemas_.DropSchema(primary_label); } + StorageInfo Storage::GetInfo() const { auto vertex_count = vertices_.size(); auto edge_count = edge_count_.load(std::memory_order_acquire); diff --git a/src/storage/v2/storage.hpp b/src/storage/v2/storage.hpp index 4839ff3ca..7f8ac6ddc 100644 --- a/src/storage/v2/storage.hpp +++ b/src/storage/v2/storage.hpp @@ -414,12 +414,14 @@ class Storage final { ConstraintsInfo ListAllConstraints() const; - bool CreateSchema(LabelId primary_label, std::vector<SchemaProperty> &schemas_types); - - bool DeleteSchema(LabelId primary_label); - SchemasInfo ListAllSchemas() const; + std::optional<Schemas::Schema> GetSchema(LabelId primary_label) const; + + bool CreateSchema(LabelId primary_label, const std::vector<SchemaProperty> &schemas_types); + + bool DropSchema(LabelId primary_label); + StorageInfo GetInfo() const; bool LockPath(); diff --git a/tests/unit/cypher_main_visitor.cpp b/tests/unit/cypher_main_visitor.cpp index 39663e2ea..f0d5a6cef 100644 --- a/tests/unit/cypher_main_visitor.cpp +++ b/tests/unit/cypher_main_visitor.cpp @@ -32,6 +32,7 @@ #include <gmock/gmock.h> #include <gtest/gtest.h> +#include "common/types.hpp" #include "query/exceptions.hpp" #include "query/frontend/ast/ast.hpp" #include "query/frontend/ast/cypher_main_visitor.hpp" @@ -2213,6 +2214,8 @@ TEST_P(CypherMainVisitorTest, GrantPrivilege) { {AuthQuery::Privilege::MODULE_READ}); check_auth_query(&ast_generator, "GRANT MODULE_WRITE TO user", AuthQuery::Action::GRANT_PRIVILEGE, "", "", "user", {}, {AuthQuery::Privilege::MODULE_WRITE}); + check_auth_query(&ast_generator, "GRANT SCHEMA TO user", AuthQuery::Action::GRANT_PRIVILEGE, "", "", "user", {}, + {AuthQuery::Privilege::SCHEMA}); } TEST_P(CypherMainVisitorTest, DenyPrivilege) { @@ -2253,6 +2256,8 @@ TEST_P(CypherMainVisitorTest, DenyPrivilege) { {AuthQuery::Privilege::MODULE_READ}); check_auth_query(&ast_generator, "DENY MODULE_WRITE TO user", AuthQuery::Action::DENY_PRIVILEGE, "", "", "user", {}, {AuthQuery::Privilege::MODULE_WRITE}); + check_auth_query(&ast_generator, "DENY SCHEMA TO user", AuthQuery::Action::DENY_PRIVILEGE, "", "", "user", {}, + {AuthQuery::Privilege::SCHEMA}); } TEST_P(CypherMainVisitorTest, RevokePrivilege) { @@ -2295,6 +2300,8 @@ TEST_P(CypherMainVisitorTest, RevokePrivilege) { {}, {AuthQuery::Privilege::MODULE_READ}); check_auth_query(&ast_generator, "REVOKE MODULE_WRITE FROM user", AuthQuery::Action::REVOKE_PRIVILEGE, "", "", "user", {}, {AuthQuery::Privilege::MODULE_WRITE}); + check_auth_query(&ast_generator, "REVOKE SCHEMA FROM user", AuthQuery::Action::REVOKE_PRIVILEGE, "", "", "user", {}, + {AuthQuery::Privilege::SCHEMA}); } TEST_P(CypherMainVisitorTest, ShowPrivileges) { @@ -4211,3 +4218,110 @@ TEST_P(CypherMainVisitorTest, Foreach) { ASSERT_TRUE(dynamic_cast<RemoveProperty *>(*++clauses.begin())); } } + +TEST_P(CypherMainVisitorTest, TestShowSchemas) { + auto &ast_generator = *GetParam(); + auto *query = dynamic_cast<SchemaQuery *>(ast_generator.ParseQuery("SHOW SCHEMAS")); + ASSERT_TRUE(query); + EXPECT_EQ(query->action_, SchemaQuery::Action::SHOW_SCHEMAS); +} + +TEST_P(CypherMainVisitorTest, TestShowSchema) { + auto &ast_generator = *GetParam(); + EXPECT_THROW(ast_generator.ParseQuery("SHOW SCHEMA ON label"), SyntaxException); + EXPECT_THROW(ast_generator.ParseQuery("SHOW SCHEMA :label"), SyntaxException); + EXPECT_THROW(ast_generator.ParseQuery("SHOW SCHEMA label"), SyntaxException); + + auto *query = dynamic_cast<SchemaQuery *>(ast_generator.ParseQuery("SHOW SCHEMA ON :label")); + ASSERT_TRUE(query); + EXPECT_EQ(query->action_, SchemaQuery::Action::SHOW_SCHEMA); + EXPECT_EQ(query->label_, ast_generator.Label("label")); +} + +void AssertSchemaPropertyMap(auto &schema_property_map, + std::vector<std::pair<std::string, memgraph::common::SchemaType>> properties_type, + auto &ast_generator) { + EXPECT_EQ(schema_property_map.size(), properties_type.size()); + for (size_t i{0}; i < schema_property_map.size(); ++i) { + // Assert PropertyId + EXPECT_EQ(schema_property_map[i].first, ast_generator.Prop(properties_type[i].first)); + // Assert Property Type + EXPECT_EQ(schema_property_map[i].second, properties_type[i].second); + } +} + +TEST_P(CypherMainVisitorTest, TestCreateSchema) { + { + auto &ast_generator = *GetParam(); + EXPECT_THROW(ast_generator.ParseQuery("CREATE SCHEMA ON :label"), SyntaxException); + EXPECT_THROW(ast_generator.ParseQuery("CREATE SCHEMA ON :label()"), SyntaxException); + EXPECT_THROW(ast_generator.ParseQuery("CREATE SCHEMA ON :label(123 INTEGER)"), SyntaxException); + EXPECT_THROW(ast_generator.ParseQuery("CREATE SCHEMA ON :label(name TYPE)"), SyntaxException); + EXPECT_THROW(ast_generator.ParseQuery("CREATE SCHEMA ON :label(name, age)"), SyntaxException); + EXPECT_THROW(ast_generator.ParseQuery("CREATE SCHEMA ON :label(name, DURATION)"), SyntaxException); + EXPECT_THROW(ast_generator.ParseQuery("CREATE SCHEMA ON label(name INTEGER)"), SyntaxException); + EXPECT_THROW(ast_generator.ParseQuery("CREATE SCHEMA ON :label(name INTEGER, name INTEGER)"), SemanticException); + EXPECT_THROW(ast_generator.ParseQuery("CREATE SCHEMA ON :label(name INTEGER, name STRING)"), SemanticException); + } + { + auto &ast_generator = *GetParam(); + auto *query = dynamic_cast<SchemaQuery *>(ast_generator.ParseQuery("CREATE SCHEMA ON :label1(name STRING)")); + ASSERT_TRUE(query); + EXPECT_EQ(query->action_, SchemaQuery::Action::CREATE_SCHEMA); + EXPECT_EQ(query->label_, ast_generator.Label("label1")); + AssertSchemaPropertyMap(query->schema_type_map_, {{"name", memgraph::common::SchemaType::STRING}}, ast_generator); + } + { + auto &ast_generator = *GetParam(); + auto *query = dynamic_cast<SchemaQuery *>(ast_generator.ParseQuery("CREATE SCHEMA ON :label2(name string)")); + ASSERT_TRUE(query); + EXPECT_EQ(query->action_, SchemaQuery::Action::CREATE_SCHEMA); + EXPECT_EQ(query->label_, ast_generator.Label("label2")); + AssertSchemaPropertyMap(query->schema_type_map_, {{"name", memgraph::common::SchemaType::STRING}}, ast_generator); + } + { + auto &ast_generator = *GetParam(); + auto *query = dynamic_cast<SchemaQuery *>( + ast_generator.ParseQuery("CREATE SCHEMA ON :label3(first_name STRING, last_name STRING)")); + ASSERT_TRUE(query); + EXPECT_EQ(query->action_, SchemaQuery::Action::CREATE_SCHEMA); + EXPECT_EQ(query->label_, ast_generator.Label("label3")); + AssertSchemaPropertyMap( + query->schema_type_map_, + {{"first_name", memgraph::common::SchemaType::STRING}, {"last_name", memgraph::common::SchemaType::STRING}}, + ast_generator); + } + { + auto &ast_generator = *GetParam(); + auto *query = dynamic_cast<SchemaQuery *>( + ast_generator.ParseQuery("CREATE SCHEMA ON :label4(name STRING, age INTEGER, dur DURATION, birthday1 " + "LOCALDATETIME, birthday2 DATE, some_time LOCALTIME, speaks_truth BOOL)")); + ASSERT_TRUE(query); + EXPECT_EQ(query->action_, SchemaQuery::Action::CREATE_SCHEMA); + EXPECT_EQ(query->label_, ast_generator.Label("label4")); + AssertSchemaPropertyMap(query->schema_type_map_, + { + {"name", memgraph::common::SchemaType::STRING}, + {"age", memgraph::common::SchemaType::INT}, + {"dur", memgraph::common::SchemaType::DURATION}, + {"birthday1", memgraph::common::SchemaType::LOCALDATETIME}, + {"birthday2", memgraph::common::SchemaType::DATE}, + {"some_time", memgraph::common::SchemaType::LOCALTIME}, + {"speaks_truth", memgraph::common::SchemaType::BOOL}, + }, + ast_generator); + } +} + +TEST_P(CypherMainVisitorTest, TestDropSchema) { + auto &ast_generator = *GetParam(); + EXPECT_THROW(ast_generator.ParseQuery("DROP SCHEMA"), SyntaxException); + EXPECT_THROW(ast_generator.ParseQuery("DROP SCHEMA ON label"), SyntaxException); + EXPECT_THROW(ast_generator.ParseQuery("DROP SCHEMA :label"), SyntaxException); + EXPECT_THROW(ast_generator.ParseQuery("DROP SCHEMA ON :label()"), SyntaxException); + + auto *query = dynamic_cast<SchemaQuery *>(ast_generator.ParseQuery("DROP SCHEMA ON :label")); + ASSERT_TRUE(query); + EXPECT_EQ(query->action_, SchemaQuery::Action::DROP_SCHEMA); + EXPECT_EQ(query->label_, ast_generator.Label("label")); +} diff --git a/tests/unit/interpreter.cpp b/tests/unit/interpreter.cpp index f5a3e03b3..466079578 100644 --- a/tests/unit/interpreter.cpp +++ b/tests/unit/interpreter.cpp @@ -10,8 +10,10 @@ // licenses/APL.txt. #include <algorithm> +#include <cstddef> #include <cstdlib> #include <filesystem> +#include <unordered_set> #include "communication/bolt/v1/value.hpp" #include "communication/result_stream_faker.hpp" @@ -38,6 +40,11 @@ auto ToEdgeList(const memgraph::communication::bolt::Value &v) { list.push_back(x.ValueEdge()); } return list; +} + +auto StringToUnorderedSet(const std::string &element) { + const auto element_split = memgraph::utils::Split(element, ", "); + return std::unordered_set<std::string>(element_split.begin(), element_split.end()); }; struct InterpreterFaker { @@ -1465,3 +1472,145 @@ TEST_F(InterpreterTest, LoadCsvClauseNotification) { "conversion functions such as ToInteger, ToFloat, ToBoolean etc."); ASSERT_EQ(notification["description"].ValueString(), ""); } + +TEST_F(InterpreterTest, CreateSchemaMulticommandTransaction) { + Interpret("BEGIN"); + ASSERT_THROW(Interpret("CREATE SCHEMA ON :label(name STRING, age INTEGER)"), + memgraph::query::ConstraintInMulticommandTxException); + Interpret("ROLLBACK"); +} + +TEST_F(InterpreterTest, ShowSchemasMulticommandTransaction) { + Interpret("BEGIN"); + ASSERT_THROW(Interpret("SHOW SCHEMAS"), memgraph::query::ConstraintInMulticommandTxException); + Interpret("ROLLBACK"); +} + +TEST_F(InterpreterTest, ShowSchemaMulticommandTransaction) { + Interpret("BEGIN"); + ASSERT_THROW(Interpret("SHOW SCHEMA ON :label"), memgraph::query::ConstraintInMulticommandTxException); + Interpret("ROLLBACK"); +} + +TEST_F(InterpreterTest, DropSchemaMulticommandTransaction) { + Interpret("BEGIN"); + ASSERT_THROW(Interpret("DROP SCHEMA ON :label"), memgraph::query::ConstraintInMulticommandTxException); + Interpret("ROLLBACK"); +} + +TEST_F(InterpreterTest, SchemaTestCreateAndShow) { + // Empty schema type map should result with syntax exception. + ASSERT_THROW(Interpret("CREATE SCHEMA ON :label();"), memgraph::query::SyntaxException); + + // Duplicate properties are should also cause an exception + ASSERT_THROW(Interpret("CREATE SCHEMA ON :label(name STRING, name STRING);"), memgraph::query::SemanticException); + ASSERT_THROW(Interpret("CREATE SCHEMA ON :label(name STRING, name INTEGER);"), memgraph::query::SemanticException); + + { + // Cannot create same schema twice + Interpret("CREATE SCHEMA ON :label(name STRING, age INTEGER)"); + ASSERT_THROW(Interpret("CREATE SCHEMA ON :label(name STRING);"), memgraph::query::QueryException); + } + // Show schema + { + auto stream = Interpret("SHOW SCHEMA ON :label"); + ASSERT_EQ(stream.GetHeader().size(), 2U); + const auto &header = stream.GetHeader(); + ASSERT_EQ(header[0], "property_name"); + ASSERT_EQ(header[1], "property_type"); + ASSERT_EQ(stream.GetResults().size(), 2U); + std::unordered_map<std::string, std::string> result_table{{"age", "Integer"}, {"name", "String"}}; + + const auto &result = stream.GetResults().front(); + ASSERT_EQ(result.size(), 2U); + const auto key1 = result[0].ValueString(); + ASSERT_TRUE(result_table.contains(key1)); + ASSERT_EQ(result[1].ValueString(), result_table[key1]); + + const auto &result2 = stream.GetResults().front(); + ASSERT_EQ(result2.size(), 2U); + const auto key2 = result2[0].ValueString(); + ASSERT_TRUE(result_table.contains(key2)); + ASSERT_EQ(result[1].ValueString(), result_table[key2]); + } + // Create Another Schema + Interpret("CREATE SCHEMA ON :label2(place STRING, dur DURATION)"); + + // Show schemas + { + auto stream = Interpret("SHOW SCHEMAS"); + ASSERT_EQ(stream.GetHeader().size(), 2U); + const auto &header = stream.GetHeader(); + ASSERT_EQ(header[0], "label"); + ASSERT_EQ(header[1], "primary_key"); + ASSERT_EQ(stream.GetResults().size(), 2U); + std::unordered_map<std::string, std::unordered_set<std::string>> result_table{ + {"label", {"name::String", "age::Integer"}}, {"label2", {"place::String", "dur::Duration"}}}; + + const auto &result = stream.GetResults().front(); + ASSERT_EQ(result.size(), 2U); + const auto key1 = result[0].ValueString(); + ASSERT_TRUE(result_table.contains(key1)); + const auto primary_key_split = StringToUnorderedSet(result[1].ValueString()); + ASSERT_EQ(primary_key_split.size(), 2); + ASSERT_TRUE(primary_key_split == result_table[key1]) << "actual value is: " << result[1].ValueString(); + + const auto &result2 = stream.GetResults().front(); + ASSERT_EQ(result2.size(), 2U); + const auto key2 = result2[0].ValueString(); + ASSERT_TRUE(result_table.contains(key2)); + const auto primary_key_split2 = StringToUnorderedSet(result2[1].ValueString()); + ASSERT_EQ(primary_key_split2.size(), 2); + ASSERT_TRUE(primary_key_split2 == result_table[key2]) << "Real value is: " << result[1].ValueString(); + } +} + +TEST_F(InterpreterTest, SchemaTestCreateDropAndShow) { + Interpret("CREATE SCHEMA ON :label(name STRING, age INTEGER)"); + // Wrong syntax for dropping schema. + ASSERT_THROW(Interpret("DROP SCHEMA ON :label();"), memgraph::query::SyntaxException); + // Cannot drop non existant schema. + ASSERT_THROW(Interpret("DROP SCHEMA ON :label1;"), memgraph::query::QueryException); + + // Create Schema and Drop + auto get_number_of_schemas = [this]() { + auto stream = Interpret("SHOW SCHEMAS"); + return stream.GetResults().size(); + }; + + ASSERT_EQ(get_number_of_schemas(), 1); + Interpret("CREATE SCHEMA ON :label1(name STRING, age INTEGER)"); + ASSERT_EQ(get_number_of_schemas(), 2); + Interpret("CREATE SCHEMA ON :label2(name STRING, sex BOOL)"); + ASSERT_EQ(get_number_of_schemas(), 3); + Interpret("DROP SCHEMA ON :label1"); + ASSERT_EQ(get_number_of_schemas(), 2); + Interpret("CREATE SCHEMA ON :label3(name STRING, birthday LOCALDATETIME)"); + ASSERT_EQ(get_number_of_schemas(), 3); + Interpret("DROP SCHEMA ON :label2"); + ASSERT_EQ(get_number_of_schemas(), 2); + Interpret("CREATE SCHEMA ON :label4(name STRING, age DURATION)"); + ASSERT_EQ(get_number_of_schemas(), 3); + Interpret("DROP SCHEMA ON :label3"); + ASSERT_EQ(get_number_of_schemas(), 2); + Interpret("DROP SCHEMA ON :label"); + ASSERT_EQ(get_number_of_schemas(), 1); + + // Show schemas + auto stream = Interpret("SHOW SCHEMAS"); + ASSERT_EQ(stream.GetHeader().size(), 2U); + const auto &header = stream.GetHeader(); + ASSERT_EQ(header[0], "label"); + ASSERT_EQ(header[1], "primary_key"); + ASSERT_EQ(stream.GetResults().size(), 1U); + std::unordered_map<std::string, std::unordered_set<std::string>> result_table{ + {"label4", {"name::String", "age::Duration"}}}; + + const auto &result = stream.GetResults().front(); + ASSERT_EQ(result.size(), 2U); + const auto key1 = result[0].ValueString(); + ASSERT_TRUE(result_table.contains(key1)); + const auto primary_key_split = StringToUnorderedSet(result[1].ValueString()); + ASSERT_EQ(primary_key_split.size(), 2); + ASSERT_TRUE(primary_key_split == result_table[key1]); +} diff --git a/tests/unit/query_required_privileges.cpp b/tests/unit/query_required_privileges.cpp index ad21b10c4..4aab492e1 100644 --- a/tests/unit/query_required_privileges.cpp +++ b/tests/unit/query_required_privileges.cpp @@ -192,6 +192,11 @@ TEST_F(TestPrivilegeExtractor, ShowVersion) { EXPECT_THAT(GetRequiredPrivileges(query), UnorderedElementsAre(AuthQuery::Privilege::STATS)); } +TEST_F(TestPrivilegeExtractor, SchemaQuery) { + auto *query = storage.Create<SchemaQuery>(); + EXPECT_THAT(GetRequiredPrivileges(query), UnorderedElementsAre(AuthQuery::Privilege::SCHEMA)); +} + TEST_F(TestPrivilegeExtractor, CallProcedureQuery) { { auto *query = QUERY(SINGLE_QUERY(CALL_PROCEDURE("mg.get_module_files")));