From 1d90b60f56b8acb1ac5a8003da362bd75caa1ed4 Mon Sep 17 00:00:00 2001 From: Andi <andi8647@gmail.com> Date: Tue, 21 Nov 2023 09:19:50 +0100 Subject: [PATCH] Add schema.assert (#1485) --- include/_mgp.hpp | 48 +++ include/mg_procedure.h | 59 +++ include/mgp.hpp | 82 +++- query_modules/schema.cpp | 465 +++++++++++++++++++++- src/query/db_accessor.cpp | 2 + src/query/db_accessor.hpp | 2 + src/query/interpreter.cpp | 5 +- src/query/procedure/mg_procedure_impl.cpp | 327 +++++++++++++++ src/storage/v2/inmemory/storage.cpp | 16 +- tests/e2e/query_modules/schema_test.py | 404 +++++++++++++++++++ tests/e2e/query_modules/workloads.yaml | 2 +- 11 files changed, 1373 insertions(+), 39 deletions(-) diff --git a/include/_mgp.hpp b/include/_mgp.hpp index fd286b6c6..58685b440 100644 --- a/include/_mgp.hpp +++ b/include/_mgp.hpp @@ -236,6 +236,54 @@ inline mgp_type *type_nullable(mgp_type *type) { return MgInvoke<mgp_type *>(mgp // mgp_graph +inline bool create_label_index(mgp_graph *graph, const char *label) { + return MgInvoke<int>(mgp_create_label_index, graph, label); +} + +inline bool drop_label_index(mgp_graph *graph, const char *label) { + return MgInvoke<int>(mgp_drop_label_index, graph, label); +} + +inline mgp_list *list_all_label_indices(mgp_graph *graph, mgp_memory *memory) { + return MgInvoke<mgp_list *>(mgp_list_all_label_indices, graph, memory); +} + +inline bool create_label_property_index(mgp_graph *graph, const char *label, const char *property) { + return MgInvoke<int>(mgp_create_label_property_index, graph, label, property); +} + +inline bool drop_label_property_index(mgp_graph *graph, const char *label, const char *property) { + return MgInvoke<int>(mgp_drop_label_property_index, graph, label, property); +} + +inline mgp_list *list_all_label_property_indices(mgp_graph *graph, mgp_memory *memory) { + return MgInvoke<mgp_list *>(mgp_list_all_label_property_indices, graph, memory); +} + +inline bool create_existence_constraint(mgp_graph *graph, const char *label, const char *property) { + return MgInvoke<int>(mgp_create_existence_constraint, graph, label, property); +} + +inline bool drop_existence_constraint(mgp_graph *graph, const char *label, const char *property) { + return MgInvoke<int>(mgp_drop_existence_constraint, graph, label, property); +} + +inline mgp_list *list_all_existence_constraints(mgp_graph *graph, mgp_memory *memory) { + return MgInvoke<mgp_list *>(mgp_list_all_existence_constraints, graph, memory); +} + +inline bool create_unique_constraint(mgp_graph *memgraph_graph, const char *label, mgp_value *properties) { + return MgInvoke<int>(mgp_create_unique_constraint, memgraph_graph, label, properties); +} + +inline bool drop_unique_constraint(mgp_graph *memgraph_graph, const char *label, mgp_value *properties) { + return MgInvoke<int>(mgp_drop_unique_constraint, memgraph_graph, label, properties); +} + +inline mgp_list *list_all_unique_constraints(mgp_graph *graph, mgp_memory *memory) { + return MgInvoke<mgp_list *>(mgp_list_all_unique_constraints, graph, memory); +} + inline bool graph_is_mutable(mgp_graph *graph) { return MgInvoke<int>(mgp_graph_is_mutable, graph); } inline mgp_vertex *graph_create_vertex(mgp_graph *graph, mgp_memory *memory) { diff --git a/include/mg_procedure.h b/include/mg_procedure.h index 0bd831174..857c5f4dd 100644 --- a/include/mg_procedure.h +++ b/include/mg_procedure.h @@ -876,6 +876,65 @@ enum mgp_error mgp_edge_iter_properties(struct mgp_edge *e, struct mgp_memory *m enum mgp_error mgp_graph_get_vertex_by_id(struct mgp_graph *g, struct mgp_vertex_id id, struct mgp_memory *memory, struct mgp_vertex **result); +/// Creates label index for given label. +/// mgp_error::MGP_ERROR_NO_ERROR is always returned. +/// if label index already exists, result will be 0, otherwise 1. +enum mgp_error mgp_create_label_index(struct mgp_graph *graph, const char *label, int *result); + +/// Drop label index. +enum mgp_error mgp_drop_label_index(struct mgp_graph *graph, const char *label, int *result); + +/// List all label indices. +enum mgp_error mgp_list_all_label_indices(struct mgp_graph *graph, struct mgp_memory *memory, struct mgp_list **result); + +/// Creates label-property index for given label and propery. +/// mgp_error::MGP_ERROR_NO_ERROR is always returned. +/// if label property index already exists, result will be 0, otherwise 1. +enum mgp_error mgp_create_label_property_index(struct mgp_graph *graph, const char *label, const char *property, + int *result); + +/// Drops label-property index for given label and propery. +/// mgp_error::MGP_ERROR_NO_ERROR is always returned. +/// if dropping label property index failed, result will be 0, otherwise 1. +enum mgp_error mgp_drop_label_property_index(struct mgp_graph *graph, const char *label, const char *property, + int *result); + +/// List all label+property indices. +enum mgp_error mgp_list_all_label_property_indices(struct mgp_graph *graph, struct mgp_memory *memory, + struct mgp_list **result); + +/// Creates existence constraint for given label and property. +/// mgp_error::MGP_ERROR_NO_ERROR is always returned. +/// if creating existence constraint failed, result will be 0, otherwise 1. +enum mgp_error mgp_create_existence_constraint(struct mgp_graph *graph, const char *label, const char *property, + int *result); + +/// Drops existence constraint for given label and property. +/// mgp_error::MGP_ERROR_NO_ERROR is always returned. +/// if dropping existence constraint failed, result will be 0, otherwise 1. +enum mgp_error mgp_drop_existence_constraint(struct mgp_graph *graph, const char *label, const char *property, + int *result); + +/// List all existence constraints. +enum mgp_error mgp_list_all_existence_constraints(struct mgp_graph *graph, struct mgp_memory *memory, + struct mgp_list **result); + +/// Creates unique constraint for given label and properties. +/// mgp_error::MGP_ERROR_NO_ERROR is always returned. +/// if creating unique constraint failed, result will be 0, otherwise 1. +enum mgp_error mgp_create_unique_constraint(struct mgp_graph *graph, const char *label, struct mgp_value *properties, + int *result); + +/// Drops unique constraint for given label and properties. +/// mgp_error::MGP_ERROR_NO_ERROR is always returned. +/// if dropping unique constraint failed, result will be 0, otherwise 1. +enum mgp_error mgp_drop_unique_constraint(struct mgp_graph *graph, const char *label, struct mgp_value *properties, + int *result); + +/// List all unique constraints +enum mgp_error mgp_list_all_unique_constraints(struct mgp_graph *graph, struct mgp_memory *memory, + struct mgp_list **result); + /// Result is non-zero if the graph can be modified. /// If a graph is immutable, then vertices cannot be created or deleted, and all of the returned vertices will be /// immutable also. The same applies for edges. diff --git a/include/mgp.hpp b/include/mgp.hpp index bea3545bb..25e365e53 100644 --- a/include/mgp.hpp +++ b/include/mgp.hpp @@ -21,12 +21,10 @@ #include <string> #include <string_view> #include <thread> -#include <unordered_map> -#include <vector> - -#include <functional> #include <type_traits> +#include <unordered_map> #include <utility> +#include <vector> #include "_mgp.hpp" #include "mg_exceptions.hpp" @@ -1289,7 +1287,7 @@ class Value { std::string_view ValueString() const; std::string_view ValueString(); /// @pre Value type needs to be Type::List. - const List ValueList() const; + List ValueList() const; List ValueList(); /// @pre Value type needs to be Type::Map. const Map ValueMap() const; @@ -3651,7 +3649,7 @@ inline std::string_view Value::ValueString() { return mgp::value_get_string(ptr_); } -inline const List Value::ValueList() const { +inline List Value::ValueList() const { if (Type() != Type::List) { throw ValueException("Type of value is wrong: expected List."); } @@ -4279,9 +4277,77 @@ inline void AddParamsReturnsToProc(mgp_proc *proc, std::vector<Parameter> ¶m } } // namespace detail +inline bool CreateLabelIndex(mgp_graph *memgaph_graph, const std::string_view label) { + return create_label_index(memgaph_graph, label.data()); +} + +inline bool DropLabelIndex(mgp_graph *memgaph_graph, const std::string_view label) { + return drop_label_index(memgaph_graph, label.data()); +} + +inline List ListAllLabelIndices(mgp_graph *memgraph_graph) { + auto *label_indices = mgp::MemHandlerCallback(list_all_label_indices, memgraph_graph); + if (label_indices == nullptr) { + throw ValueException("Couldn't list all label indices"); + } + return List(label_indices); +} + +inline bool CreateLabelPropertyIndex(mgp_graph *memgaph_graph, const std::string_view label, + const std::string_view property) { + return create_label_property_index(memgaph_graph, label.data(), property.data()); +} + +inline bool DropLabelPropertyIndex(mgp_graph *memgaph_graph, const std::string_view label, + const std::string_view property) { + return drop_label_property_index(memgaph_graph, label.data(), property.data()); +} + +inline List ListAllLabelPropertyIndices(mgp_graph *memgraph_graph) { + auto *label_property_indices = mgp::MemHandlerCallback(list_all_label_property_indices, memgraph_graph); + if (label_property_indices == nullptr) { + throw ValueException("Couldn't list all label+property indices"); + } + return List(label_property_indices); +} + +inline bool CreateExistenceConstraint(mgp_graph *memgraph_graph, const std::string_view label, + const std::string_view property) { + return create_existence_constraint(memgraph_graph, label.data(), property.data()); +} + +inline bool DropExistenceConstraint(mgp_graph *memgraph_graph, const std::string_view label, + const std::string_view property) { + return drop_existence_constraint(memgraph_graph, label.data(), property.data()); +} + +inline List ListAllExistenceConstraints(mgp_graph *memgraph_graph) { + auto *existence_constraints = mgp::MemHandlerCallback(list_all_existence_constraints, memgraph_graph); + if (existence_constraints == nullptr) { + throw ValueException("Couldn't list all existence_constraints"); + } + return List(existence_constraints); +} + +inline bool CreateUniqueConstraint(mgp_graph *memgraph_graph, const std::string_view label, mgp_value *properties) { + return create_unique_constraint(memgraph_graph, label.data(), properties); +} + +inline bool DropUniqueConstraint(mgp_graph *memgraph_graph, const std::string_view label, mgp_value *properties) { + return drop_unique_constraint(memgraph_graph, label.data(), properties); +} + +inline List ListAllUniqueConstraints(mgp_graph *memgraph_graph) { + auto *unique_constraints = mgp::MemHandlerCallback(list_all_unique_constraints, memgraph_graph); + if (unique_constraints == nullptr) { + throw ValueException("Couldn't list all unique_constraints"); + } + return List(unique_constraints); +} + void AddProcedure(mgp_proc_cb callback, std::string_view name, ProcedureType proc_type, std::vector<Parameter> parameters, std::vector<Return> returns, mgp_module *module, - mgp_memory *memory) { + mgp_memory * /*memory*/) { auto *proc = (proc_type == ProcedureType::Read) ? mgp::module_add_read_procedure(module, name.data(), callback) : mgp::module_add_write_procedure(module, name.data(), callback); detail::AddParamsReturnsToProc(proc, parameters, returns); @@ -4289,7 +4355,7 @@ void AddProcedure(mgp_proc_cb callback, std::string_view name, ProcedureType pro void AddBatchProcedure(mgp_proc_cb callback, mgp_proc_initializer initializer, mgp_proc_cleanup cleanup, std::string_view name, ProcedureType proc_type, std::vector<Parameter> parameters, - std::vector<Return> returns, mgp_module *module, mgp_memory *memory) { + std::vector<Return> returns, mgp_module *module, mgp_memory * /*memory*/) { auto *proc = (proc_type == ProcedureType::Read) ? mgp::module_add_batch_read_procedure(module, name.data(), callback, initializer, cleanup) : mgp::module_add_batch_write_procedure(module, name.data(), callback, initializer, cleanup); diff --git a/query_modules/schema.cpp b/query_modules/schema.cpp index 436e00716..d5a657e98 100644 --- a/query_modules/schema.cpp +++ b/query_modules/schema.cpp @@ -10,18 +10,33 @@ // licenses/APL.txt. #include <mgp.hpp> +#include "utils/string.hpp" + +#include <optional> namespace Schema { -/*NodeTypeProperties and RelTypeProperties constants*/ +constexpr std::string_view kStatusKept = "Kept"; +constexpr std::string_view kStatusCreated = "Created"; +constexpr std::string_view kStatusDropped = "Dropped"; constexpr std::string_view kReturnNodeType = "nodeType"; constexpr std::string_view kProcedureNodeType = "node_type_properties"; constexpr std::string_view kProcedureRelType = "rel_type_properties"; +constexpr std::string_view kProcedureAssert = "assert"; constexpr std::string_view kReturnLabels = "nodeLabels"; constexpr std::string_view kReturnRelType = "relType"; constexpr std::string_view kReturnPropertyName = "propertyName"; constexpr std::string_view kReturnPropertyType = "propertyTypes"; constexpr std::string_view kReturnMandatory = "mandatory"; +constexpr std::string_view kReturnLabel = "label"; +constexpr std::string_view kReturnKey = "key"; +constexpr std::string_view kReturnKeys = "keys"; +constexpr std::string_view kReturnUnique = "unique"; +constexpr std::string_view kReturnAction = "action"; +constexpr std::string_view kParameterIndices = "indices"; +constexpr std::string_view kParameterUniqueConstraints = "unique_constraints"; +constexpr std::string_view kParameterExistenceConstraints = "existence_constraints"; +constexpr std::string_view kParameterDropExisting = "drop_existing"; std::string TypeOf(const mgp::Type &type); @@ -35,6 +50,7 @@ void ProcessPropertiesRel(mgp::Record &record, const std::string_view &type, con void NodeTypeProperties(mgp_list *args, mgp_graph *memgraph_graph, mgp_result *result, mgp_memory *memory); void RelTypeProperties(mgp_list *args, mgp_graph *memgraph_graph, mgp_result *result, mgp_memory *memory); +void Assert(mgp_list *args, mgp_graph *memgraph_graph, mgp_result *result, mgp_memory *memory); } // namespace Schema /*we have << operator for type in Cpp API, but in it we return somewhat different strings than I would like in this @@ -92,21 +108,22 @@ void Schema::ProcessPropertiesRel(mgp::Record &record, const std::string_view &t record.Insert(std::string(kReturnMandatory).c_str(), mandatory); } -void Schema::NodeTypeProperties(mgp_list *args, mgp_graph *memgraph_graph, mgp_result *result, mgp_memory *memory) { +void Schema::NodeTypeProperties(mgp_list * /*args*/, mgp_graph *memgraph_graph, mgp_result *result, + mgp_memory *memory) { mgp::MemoryDispatcherGuard guard{memory}; ; const auto record_factory = mgp::RecordFactory(result); try { const mgp::Graph graph = mgp::Graph(memgraph_graph); for (auto node : graph.Nodes()) { - std::string type = ""; + std::string type; mgp::List labels = mgp::List(); for (auto label : node.Labels()) { labels.AppendExtend(mgp::Value(label)); type += ":`" + std::string(label) + "`"; } - if (node.Properties().size() == 0) { + if (node.Properties().empty()) { auto record = record_factory.NewRecord(); ProcessPropertiesNode<std::string>(record, type, labels, "", "", false); continue; @@ -126,16 +143,15 @@ void Schema::NodeTypeProperties(mgp_list *args, mgp_graph *memgraph_graph, mgp_r } } -void Schema::RelTypeProperties(mgp_list *args, mgp_graph *memgraph_graph, mgp_result *result, mgp_memory *memory) { +void Schema::RelTypeProperties(mgp_list * /*args*/, mgp_graph *memgraph_graph, mgp_result *result, mgp_memory *memory) { mgp::MemoryDispatcherGuard guard{memory}; - ; const auto record_factory = mgp::RecordFactory(result); try { const mgp::Graph graph = mgp::Graph(memgraph_graph); for (auto rel : graph.Relationships()) { std::string type = ":`" + std::string(rel.Type()) + "`"; - if (rel.Properties().size() == 0) { + if (rel.Properties().empty()) { auto record = record_factory.NewRecord(); ProcessPropertiesRel<std::string>(record, type, "", "", false); continue; @@ -155,29 +171,436 @@ void Schema::RelTypeProperties(mgp_list *args, mgp_graph *memgraph_graph, mgp_re } } +void InsertRecordForLabelIndex(const auto &record_factory, const std::string_view label, + const std::string_view status) { + auto record = record_factory.NewRecord(); + record.Insert(std::string(Schema::kReturnLabel).c_str(), label); + record.Insert(std::string(Schema::kReturnKey).c_str(), ""); + record.Insert(std::string(Schema::kReturnKeys).c_str(), mgp::List()); + record.Insert(std::string(Schema::kReturnUnique).c_str(), false); + record.Insert(std::string(Schema::kReturnAction).c_str(), status); +} + +void InsertRecordForUniqueConstraint(const auto &record_factory, const std::string_view label, + const mgp::List &properties, const std::string_view status) { + auto record = record_factory.NewRecord(); + record.Insert(std::string(Schema::kReturnLabel).c_str(), label); + record.Insert(std::string(Schema::kReturnKey).c_str(), properties.ToString()); + record.Insert(std::string(Schema::kReturnKeys).c_str(), properties); + record.Insert(std::string(Schema::kReturnUnique).c_str(), true); + record.Insert(std::string(Schema::kReturnAction).c_str(), status); +} + +void InsertRecordForLabelPropertyIndexAndExistenceConstraint(const auto &record_factory, const std::string_view label, + const std::string_view property, + const std::string_view status) { + auto record = record_factory.NewRecord(); + record.Insert(std::string(Schema::kReturnLabel).c_str(), label); + record.Insert(std::string(Schema::kReturnKey).c_str(), property); + record.Insert(std::string(Schema::kReturnKeys).c_str(), mgp::List({mgp::Value(property)})); + record.Insert(std::string(Schema::kReturnUnique).c_str(), false); + record.Insert(std::string(Schema::kReturnAction).c_str(), status); +} + +void ProcessCreatingLabelIndex(const std::string_view label, const std::set<std::string_view> &existing_label_indices, + mgp_graph *memgraph_graph, const auto &record_factory) { + if (existing_label_indices.contains(label)) { + InsertRecordForLabelIndex(record_factory, label, Schema::kStatusKept); + } else if (mgp::CreateLabelIndex(memgraph_graph, label)) { + InsertRecordForLabelIndex(record_factory, label, Schema::kStatusCreated); + } +} + +template <typename TFunc> +void ProcessCreatingLabelPropertyIndexAndExistenceConstraint(const std::string_view label, + const std::string_view property, + const std::set<std::string_view> &existing_collection, + const TFunc &func_creation, mgp_graph *memgraph_graph, + const auto &record_factory) { + const auto label_property_search_key = std::string(label) + ":" + std::string(property); + if (existing_collection.contains(label_property_search_key)) { + InsertRecordForLabelPropertyIndexAndExistenceConstraint(record_factory, label, property, Schema::kStatusKept); + } else if (func_creation(memgraph_graph, label, property)) { + InsertRecordForLabelPropertyIndexAndExistenceConstraint(record_factory, label, property, Schema::kStatusCreated); + } +} + +/// We collect properties for which index was created. +using AssertedIndices = std::set<std::string, std::less<>>; +AssertedIndices CreateIndicesForLabel(const std::string_view label, const mgp::Value &properties_val, + mgp_graph *memgraph_graph, const auto &record_factory, + const std::set<std::string_view> &existing_label_indices, + const std::set<std::string_view> &existing_label_property_indices) { + AssertedIndices asserted_indices; + if (!properties_val.IsList()) { + return {}; + } + if (const auto properties = properties_val.ValueList(); + properties.Empty() && mgp::CreateLabelIndex(memgraph_graph, label)) { + InsertRecordForLabelIndex(record_factory, label, Schema::kStatusCreated); + asserted_indices.emplace(""); + } else { + std::for_each(properties.begin(), properties.end(), + [&label, &existing_label_indices, &existing_label_property_indices, &memgraph_graph, &record_factory, + &asserted_indices](const mgp::Value &property) { + if (!property.IsString()) { + return; + } + const auto property_str = property.ValueString(); + if (property_str.empty()) { + ProcessCreatingLabelIndex(label, existing_label_indices, memgraph_graph, record_factory); + asserted_indices.emplace(""); + } else { + ProcessCreatingLabelPropertyIndexAndExistenceConstraint( + label, property_str, existing_label_property_indices, mgp::CreateLabelPropertyIndex, + memgraph_graph, record_factory); + asserted_indices.emplace(property_str); + } + }); + } + return asserted_indices; +} + +void ProcessIndices(const mgp::Map &indices_map, mgp_graph *memgraph_graph, const auto &record_factory, + bool drop_existing) { + auto mgp_existing_label_indices = mgp::ListAllLabelIndices(memgraph_graph); + auto mgp_existing_label_property_indices = mgp::ListAllLabelPropertyIndices(memgraph_graph); + + std::set<std::string_view> existing_label_indices; + std::transform(mgp_existing_label_indices.begin(), mgp_existing_label_indices.end(), + std::inserter(existing_label_indices, existing_label_indices.begin()), + [](const mgp::Value &index) { return index.ValueString(); }); + + std::set<std::string_view> existing_label_property_indices; + std::transform(mgp_existing_label_property_indices.begin(), mgp_existing_label_property_indices.end(), + std::inserter(existing_label_property_indices, existing_label_property_indices.begin()), + [](const mgp::Value &index) { return index.ValueString(); }); + + std::set<std::string> asserted_label_indices; + std::set<std::string> asserted_label_property_indices; + + auto merge_label_property = [](const std::string &label, const std::string &property) { + return label + ":" + property; + }; + + for (const auto &index : indices_map) { + const std::string_view label = index.key; + const mgp::Value &properties_val = index.value; + + AssertedIndices asserted_indices_new = CreateIndicesForLabel( + label, properties_val, memgraph_graph, record_factory, existing_label_indices, existing_label_property_indices); + + if (!drop_existing) { + continue; + } + std::ranges::for_each(asserted_indices_new, [&asserted_label_indices, &asserted_label_property_indices, label, + &merge_label_property](const std::string &property) { + if (property.empty()) { + asserted_label_indices.emplace(label); + } else { + asserted_label_property_indices.emplace(merge_label_property(std::string(label), property)); + } + }); + } + + if (!drop_existing) { + return; + } + + std::set<std::string_view> label_indices_to_drop; + std::ranges::set_difference(existing_label_indices, asserted_label_indices, + std::inserter(label_indices_to_drop, label_indices_to_drop.begin())); + + std::ranges::for_each(label_indices_to_drop, [memgraph_graph, &record_factory](const std::string_view label) { + if (mgp::DropLabelIndex(memgraph_graph, label)) { + InsertRecordForLabelIndex(record_factory, label, Schema::kStatusDropped); + } + }); + + std::set<std::string_view> label_property_indices_to_drop; + std::ranges::set_difference(existing_label_property_indices, asserted_label_property_indices, + std::inserter(label_property_indices_to_drop, label_property_indices_to_drop.begin())); + + auto decouple_label_property = [](const std::string_view label_property) { + const auto label_size = label_property.find(':'); + const auto label = std::string(label_property.substr(0, label_size)); + const auto property = std::string(label_property.substr(label_size + 1)); + return std::make_pair(label, property); + }; + + std::ranges::for_each(label_property_indices_to_drop, [memgraph_graph, &record_factory, decouple_label_property]( + const std::string_view label_property) { + const auto [label, property] = decouple_label_property(label_property); + if (mgp::DropLabelPropertyIndex(memgraph_graph, label, property)) { + InsertRecordForLabelPropertyIndexAndExistenceConstraint(record_factory, label, property, Schema::kStatusDropped); + } + }); +} + +using ExistenceConstraintsStorage = std::set<std::string_view>; + +ExistenceConstraintsStorage CreateExistenceConstraintsForLabel( + const std::string_view label, const mgp::Value &properties_val, mgp_graph *memgraph_graph, + const auto &record_factory, const std::set<std::string_view> &existing_existence_constraints) { + ExistenceConstraintsStorage asserted_existence_constraints; + if (!properties_val.IsList()) { + return asserted_existence_constraints; + } + + auto validate_property = [](const mgp::Value &property) -> bool { + return property.IsString() && !property.ValueString().empty(); + }; + + const auto &properties = properties_val.ValueList(); + std::for_each(properties.begin(), properties.end(), + [&label, &existing_existence_constraints, &asserted_existence_constraints, &memgraph_graph, + &record_factory, &validate_property](const mgp::Value &property) { + if (!validate_property(property)) { + return; + } + const std::string_view property_str = property.ValueString(); + asserted_existence_constraints.emplace(property_str); + ProcessCreatingLabelPropertyIndexAndExistenceConstraint( + label, property_str, existing_existence_constraints, mgp::CreateExistenceConstraint, + memgraph_graph, record_factory); + }); + return asserted_existence_constraints; +} + +void ProcessExistenceConstraints(const mgp::Map &existence_constraints_map, mgp_graph *memgraph_graph, + const auto &record_factory, bool drop_existing) { + auto mgp_existing_existence_constraints = mgp::ListAllExistenceConstraints(memgraph_graph); + std::set<std::string_view> existing_existence_constraints; + std::transform(mgp_existing_existence_constraints.begin(), mgp_existing_existence_constraints.end(), + std::inserter(existing_existence_constraints, existing_existence_constraints.begin()), + [](const mgp::Value &constraint) { return constraint.ValueString(); }); + + auto merge_label_property = [](const std::string_view label, const std::string_view property) { + auto str = std::string(label) + ":"; + str += property; + return str; + }; + + ExistenceConstraintsStorage asserted_existence_constraints; + + for (const auto &existing_constraint : existence_constraints_map) { + const std::string_view label = existing_constraint.key; + const mgp::Value &properties_val = existing_constraint.value; + auto asserted_existence_constraints_new = CreateExistenceConstraintsForLabel( + label, properties_val, memgraph_graph, record_factory, existing_existence_constraints); + if (!drop_existing) { + continue; + } + + std::ranges::for_each(asserted_existence_constraints_new, [&asserted_existence_constraints, &merge_label_property, + label](const std::string_view property) { + asserted_existence_constraints.emplace(merge_label_property(label, property)); + }); + } + + if (!drop_existing) { + return; + } + + std::set<std::string_view> existence_constraints_to_drop; + std::ranges::set_difference(existing_existence_constraints, asserted_existence_constraints, + std::inserter(existence_constraints_to_drop, existence_constraints_to_drop.begin())); + + auto decouple_label_property = [](const std::string_view label_property) { + const auto label_size = label_property.find(':'); + const auto label = std::string(label_property.substr(0, label_size)); + const auto property = std::string(label_property.substr(label_size + 1)); + return std::make_pair(label, property); + }; + + std::ranges::for_each(existence_constraints_to_drop, [&](const std::string_view label_property) { + const auto [label, property] = decouple_label_property(label_property); + if (mgp::DropExistenceConstraint(memgraph_graph, label, property)) { + InsertRecordForLabelPropertyIndexAndExistenceConstraint(record_factory, label, property, Schema::kStatusDropped); + } + }); +} + +using AssertedUniqueConstraintsStorage = std::set<std::set<std::string_view>>; +AssertedUniqueConstraintsStorage CreateUniqueConstraintsForLabel( + const std::string_view label, const mgp::Value &unique_props_nested, + const std::map<std::string_view, AssertedUniqueConstraintsStorage> &existing_unique_constraints, + mgp_graph *memgraph_graph, const auto &record_factory) { + AssertedUniqueConstraintsStorage asserted_unique_constraints; + if (!unique_props_nested.IsList()) { + return asserted_unique_constraints; + } + + auto validate_unique_constraint_props = [](const mgp::Value &properties) -> bool { + if (!properties.IsList()) { + return false; + } + const auto &properties_list = properties.ValueList(); + if (properties_list.Empty()) { + return false; + } + return std::all_of(properties_list.begin(), properties_list.end(), [](const mgp::Value &property) { + return property.IsString() && !property.ValueString().empty(); + }); + }; + + auto unique_constraint_exists = + [](const std::string_view label, const std::set<std::string_view> &properties, + const std::map<std::string_view, AssertedUniqueConstraintsStorage> &existing_unique_constraints) -> bool { + auto iter = existing_unique_constraints.find(label); + if (iter == existing_unique_constraints.end()) { + return false; + } + return iter->second.find(properties) != iter->second.end(); + }; + + for (const auto unique_props_nested_list = unique_props_nested.ValueList(); + const auto &properties : unique_props_nested_list) { + if (!validate_unique_constraint_props(properties)) { + continue; + } + const auto properties_list = properties.ValueList(); + std::set<std::string_view> properties_coll; + std::transform(properties_list.begin(), properties_list.end(), + std::inserter(properties_coll, properties_coll.begin()), + [](const mgp::Value &property) { return property.ValueString(); }); + + if (unique_constraint_exists(label, properties_coll, existing_unique_constraints)) { + InsertRecordForUniqueConstraint(record_factory, label, properties_list, Schema::kStatusKept); + } else if (mgp::CreateUniqueConstraint(memgraph_graph, label, properties.ptr())) { + InsertRecordForUniqueConstraint(record_factory, label, properties_list, Schema::kStatusCreated); + } + asserted_unique_constraints.emplace(std::move(properties_coll)); + } + return asserted_unique_constraints; +} + +void ProcessUniqueConstraints(const mgp::Map &unique_constraints_map, mgp_graph *memgraph_graph, + const auto &record_factory, bool drop_existing) { + auto mgp_existing_unique_constraints = mgp::ListAllUniqueConstraints(memgraph_graph); + // label-unique_constraints pair + std::map<std::string_view, AssertedUniqueConstraintsStorage> existing_unique_constraints; + for (const auto &constraint : mgp_existing_unique_constraints) { + auto constraint_list = constraint.ValueList(); + std::set<std::string_view> properties; + for (int i = 1; i < constraint_list.Size(); i++) { + properties.emplace(constraint_list[i].ValueString()); + } + const std::string_view label = constraint_list[0].ValueString(); + auto [it, inserted] = existing_unique_constraints.try_emplace(label, AssertedUniqueConstraintsStorage{properties}); + if (!inserted) { + it->second.emplace(std::move(properties)); + } + } + + std::map<std::string_view, AssertedUniqueConstraintsStorage> asserted_unique_constraints; + + for (const auto &[label, unique_props_nested] : unique_constraints_map) { + auto asserted_unique_constraints_new = CreateUniqueConstraintsForLabel( + label, unique_props_nested, existing_unique_constraints, memgraph_graph, record_factory); + if (drop_existing) { + asserted_unique_constraints.emplace(label, std::move(asserted_unique_constraints_new)); + } + } + + if (!drop_existing) { + return; + } + + std::vector<std::pair<std::string_view, std::set<std::string_view>>> unique_constraints_to_drop; + + // Check for each label for we found existing constraint in the DB whether it was asserted. + // If no unique constraint was found with label, we can drop all unique constraints for this label. (if branch) + // If some unique constraint was found with label, we can drop only those unique constraints that were not asserted. + // (else branch.) + std::ranges::for_each(existing_unique_constraints, [&asserted_unique_constraints, &unique_constraints_to_drop]( + const auto &existing_label_unique_constraints) { + const auto &label = existing_label_unique_constraints.first; + const auto &existing_unique_constraints_for_label = existing_label_unique_constraints.second; + const auto &asserted_unique_constraints_for_label = asserted_unique_constraints.find(label); + if (asserted_unique_constraints_for_label == asserted_unique_constraints.end()) { + std::ranges::for_each( + std::make_move_iterator(existing_unique_constraints_for_label.begin()), + std::make_move_iterator(existing_unique_constraints_for_label.end()), + [&unique_constraints_to_drop, &label](std::set<std::string_view> existing_unique_constraint_for_label) { + unique_constraints_to_drop.emplace_back(label, std::move(existing_unique_constraint_for_label)); + }); + } else { + const auto &asserted_unique_constraints_for_label_coll = asserted_unique_constraints_for_label->second; + std::ranges::for_each( + std::make_move_iterator(existing_unique_constraints_for_label.begin()), + std::make_move_iterator(existing_unique_constraints_for_label.end()), + [&unique_constraints_to_drop, &label, &asserted_unique_constraints_for_label_coll]( + std::set<std::string_view> existing_unique_constraint_for_label) { + if (!asserted_unique_constraints_for_label_coll.contains(existing_unique_constraint_for_label)) { + unique_constraints_to_drop.emplace_back(label, std::move(existing_unique_constraint_for_label)); + } + }); + } + }); + std::ranges::for_each( + unique_constraints_to_drop, [memgraph_graph, &record_factory](const auto &label_unique_constraint) { + const auto &[label, unique_constraint] = label_unique_constraint; + + auto unique_constraint_list = mgp::List(); + std::ranges::for_each(unique_constraint, [&unique_constraint_list](const std::string_view &property) { + unique_constraint_list.AppendExtend(mgp::Value(property)); + }); + + if (mgp::DropUniqueConstraint(memgraph_graph, label, mgp::Value(unique_constraint_list).ptr())) { + InsertRecordForUniqueConstraint(record_factory, label, unique_constraint_list, Schema::kStatusDropped); + } + }); +} + +void Schema::Assert(mgp_list *args, mgp_graph *memgraph_graph, mgp_result *result, mgp_memory *memory) { + mgp::MemoryDispatcherGuard guard{memory}; + const auto record_factory = mgp::RecordFactory(result); + auto arguments = mgp::List(args); + auto indices_map = arguments[0].ValueMap(); + auto unique_constraints_map = arguments[1].ValueMap(); + auto existence_constraints_map = arguments[2].ValueMap(); + auto drop_existing = arguments[3].ValueBool(); + + ProcessIndices(indices_map, memgraph_graph, record_factory, drop_existing); + ProcessExistenceConstraints(existence_constraints_map, memgraph_graph, record_factory, drop_existing); + ProcessUniqueConstraints(unique_constraints_map, memgraph_graph, record_factory, drop_existing); +} + extern "C" int mgp_init_module(struct mgp_module *module, struct mgp_memory *memory) { try { mgp::MemoryDispatcherGuard guard{memory}; ; - AddProcedure(Schema::NodeTypeProperties, std::string(Schema::kProcedureNodeType).c_str(), mgp::ProcedureType::Read, - {}, - {mgp::Return(std::string(Schema::kReturnNodeType).c_str(), mgp::Type::String), - mgp::Return(std::string(Schema::kReturnLabels).c_str(), {mgp::Type::List, mgp::Type::String}), - mgp::Return(std::string(Schema::kReturnPropertyName).c_str(), mgp::Type::String), - mgp::Return(std::string(Schema::kReturnPropertyType).c_str(), mgp::Type::Any), - mgp::Return(std::string(Schema::kReturnMandatory).c_str(), mgp::Type::Bool)}, + AddProcedure(Schema::NodeTypeProperties, Schema::kProcedureNodeType, mgp::ProcedureType::Read, {}, + {mgp::Return(Schema::kReturnNodeType, mgp::Type::String), + mgp::Return(Schema::kReturnLabels, {mgp::Type::List, mgp::Type::String}), + mgp::Return(Schema::kReturnPropertyName, mgp::Type::String), + mgp::Return(Schema::kReturnPropertyType, mgp::Type::Any), + mgp::Return(Schema::kReturnMandatory, mgp::Type::Bool)}, module, memory); - AddProcedure(Schema::RelTypeProperties, std::string(Schema::kProcedureRelType).c_str(), mgp::ProcedureType::Read, - {}, - {mgp::Return(std::string(Schema::kReturnRelType).c_str(), mgp::Type::String), - mgp::Return(std::string(Schema::kReturnPropertyName).c_str(), mgp::Type::String), - mgp::Return(std::string(Schema::kReturnPropertyType).c_str(), mgp::Type::Any), - mgp::Return(std::string(Schema::kReturnMandatory).c_str(), mgp::Type::Bool)}, + AddProcedure(Schema::RelTypeProperties, Schema::kProcedureRelType, mgp::ProcedureType::Read, {}, + {mgp::Return(Schema::kReturnRelType, mgp::Type::String), + mgp::Return(Schema::kReturnPropertyName, mgp::Type::String), + mgp::Return(Schema::kReturnPropertyType, mgp::Type::Any), + mgp::Return(Schema::kReturnMandatory, mgp::Type::Bool)}, module, memory); - + AddProcedure( + Schema::Assert, Schema::kProcedureAssert, mgp::ProcedureType::Read, + { + mgp::Parameter(Schema::kParameterIndices, {mgp::Type::Map, mgp::Type::Any}), + mgp::Parameter(Schema::kParameterUniqueConstraints, {mgp::Type::Map, mgp::Type::Any}), + mgp::Parameter(Schema::kParameterExistenceConstraints, {mgp::Type::Map, mgp::Type::Any}, + mgp::Value(mgp::Map{})), + mgp::Parameter(Schema::kParameterDropExisting, mgp::Type::Bool, mgp::Value(true)), + }, + {mgp::Return(Schema::kReturnLabel, mgp::Type::String), mgp::Return(Schema::kReturnKey, mgp::Type::String), + mgp::Return(Schema::kReturnKeys, {mgp::Type::List, mgp::Type::String}), + mgp::Return(Schema::kReturnUnique, mgp::Type::Bool), mgp::Return(Schema::kReturnAction, mgp::Type::String)}, + module, memory); } catch (const std::exception &e) { + std::cerr << "Error while initializing query module: " << e.what() << std::endl; return 1; } diff --git a/src/query/db_accessor.cpp b/src/query/db_accessor.cpp index 0250ab695..df3fb808a 100644 --- a/src/query/db_accessor.cpp +++ b/src/query/db_accessor.cpp @@ -139,6 +139,8 @@ std::optional<VertexAccessor> SubgraphDbAccessor::FindVertex(storage::Gid gid, s query::Graph *SubgraphDbAccessor::getGraph() { return graph_; } +DbAccessor *SubgraphDbAccessor::GetAccessor() { return &db_accessor_; } + VertexAccessor SubgraphVertexAccessor::GetVertexAccessor() const { return impl_; } storage::Result<EdgeVertexAccessorResult> SubgraphVertexAccessor::OutEdges(storage::View view) const { diff --git a/src/query/db_accessor.hpp b/src/query/db_accessor.hpp index d6114edaf..75ec1e9ae 100644 --- a/src/query/db_accessor.hpp +++ b/src/query/db_accessor.hpp @@ -694,6 +694,8 @@ class SubgraphDbAccessor final { std::optional<VertexAccessor> FindVertex(storage::Gid gid, storage::View view); Graph *getGraph(); + + DbAccessor *GetAccessor(); }; } // namespace memgraph::query diff --git a/src/query/interpreter.cpp b/src/query/interpreter.cpp index 30bb4eca2..5aad0ff07 100644 --- a/src/query/interpreter.cpp +++ b/src/query/interpreter.cpp @@ -147,6 +147,8 @@ void memgraph::query::CurrentDB::CleanupDBTransaction(bool abort) { namespace memgraph::query { +constexpr std::string_view kSchemaAssert = "SCHEMA.ASSERT"; + template <typename> constexpr auto kAlwaysFalse = false; @@ -3715,7 +3717,8 @@ Interpreter::PrepareResult Interpreter::Prepare(const std::string &query_string, // TODO: ATM only a single database, will change when we have multiple database transactions bool could_commit = utils::Downcast<CypherQuery>(parsed_query.query) != nullptr; bool unique = utils::Downcast<IndexQuery>(parsed_query.query) != nullptr || - utils::Downcast<ConstraintQuery>(parsed_query.query) != nullptr; + utils::Downcast<ConstraintQuery>(parsed_query.query) != nullptr || + upper_case_query.find(kSchemaAssert) != std::string::npos; SetupDatabaseTransaction(could_commit, unique); } diff --git a/src/query/procedure/mg_procedure_impl.cpp b/src/query/procedure/mg_procedure_impl.cpp index 2a176f2ed..f87377ba5 100644 --- a/src/query/procedure/mg_procedure_impl.cpp +++ b/src/query/procedure/mg_procedure_impl.cpp @@ -2535,6 +2535,333 @@ mgp_error mgp_graph_get_vertex_by_id(mgp_graph *graph, mgp_vertex_id id, mgp_mem result); } +mgp_error mgp_create_label_index(mgp_graph *graph, const char *label, int *result) { + return WrapExceptions( + [graph, label]() { + const auto label_id = std::visit([label](auto *impl) { return impl->NameToLabel(label); }, graph->impl); + const auto index_res = + std::visit(memgraph::utils::Overloaded{ + [label_id](memgraph::query::DbAccessor *impl) { return impl->CreateIndex(label_id); }, + [label_id](memgraph::query::SubgraphDbAccessor *impl) { + return impl->GetAccessor()->CreateIndex(label_id); + }}, + graph->impl); + return index_res.HasError() ? 0 : 1; + }, + result); +} + +mgp_error mgp_drop_label_index(mgp_graph *graph, const char *label, int *result) { + return WrapExceptions( + [graph, label]() { + const auto label_id = std::visit([label](auto *impl) { return impl->NameToLabel(label); }, graph->impl); + const auto index_res = + std::visit(memgraph::utils::Overloaded{ + [label_id](memgraph::query::DbAccessor *impl) { return impl->DropIndex(label_id); }, + [label_id](memgraph::query::SubgraphDbAccessor *impl) { + return impl->GetAccessor()->DropIndex(label_id); + }}, + graph->impl); + return index_res.HasError() ? 0 : 1; + }, + result); +} + +mgp_error mgp_list_all_label_indices(mgp_graph *graph, mgp_memory *memory, mgp_list **result) { + return WrapExceptions([graph, memory, result]() { + const auto index_res = std::visit( + memgraph::utils::Overloaded{ + [](memgraph::query::DbAccessor *impl) { return impl->ListAllIndices().label; }, + [](memgraph::query::SubgraphDbAccessor *impl) { return impl->GetAccessor()->ListAllIndices().label; }}, + graph->impl); + if (const auto err = mgp_list_make_empty(index_res.size(), memory, result); err != mgp_error::MGP_ERROR_NO_ERROR) { + throw std::logic_error("Listing all label indices failed due to failure of creating list"); + } + for (const auto &label : index_res) { + const auto label_id_str = std::visit([label](const auto *impl) { return impl->LabelToName(label); }, graph->impl); + + mgp_value *label_value = nullptr; + if (const auto err_str = mgp_value_make_string(label_id_str.c_str(), memory, &label_value); + err_str != mgp_error::MGP_ERROR_NO_ERROR) { + throw std::logic_error("Listing all label indices failed due to failure of creating label value"); + } + if (const auto err_list = mgp_list_append_extend(*result, label_value); + err_list != mgp_error::MGP_ERROR_NO_ERROR) { + throw std::logic_error("Listing all label indices failed due to failure of appending label value"); + } + mgp_value_destroy(label_value); + } + }); +} + +mgp_error mgp_create_label_property_index(mgp_graph *graph, const char *label, const char *property, int *result) { + return WrapExceptions( + [graph, label, property]() { + const auto label_id = std::visit([label](auto *impl) { return impl->NameToLabel(label); }, graph->impl); + const auto property_id = + std::visit([property](auto *impl) { return impl->NameToProperty(property); }, graph->impl); + const auto index_res = + std::visit(memgraph::utils::Overloaded{[label_id, property_id](memgraph::query::DbAccessor *impl) { + return impl->CreateIndex(label_id, property_id); + }, + [label_id, property_id](memgraph::query::SubgraphDbAccessor *impl) { + return impl->GetAccessor()->CreateIndex(label_id, property_id); + }}, + graph->impl); + return index_res.HasError() ? 0 : 1; + }, + result); +} + +mgp_error mgp_drop_label_property_index(mgp_graph *graph, const char *label, const char *property, int *result) { + return WrapExceptions( + [graph, label, property]() { + const auto label_id = std::visit([label](auto *impl) { return impl->NameToLabel(label); }, graph->impl); + const auto property_id = + std::visit([property](auto *impl) { return impl->NameToProperty(property); }, graph->impl); + const auto index_res = + std::visit(memgraph::utils::Overloaded{[label_id, property_id](memgraph::query::DbAccessor *impl) { + return impl->DropIndex(label_id, property_id); + }, + [label_id, property_id](memgraph::query::SubgraphDbAccessor *impl) { + return impl->GetAccessor()->DropIndex(label_id, property_id); + }}, + graph->impl); + return index_res.HasError() ? 0 : 1; + }, + result); +} + +mgp_error create_and_append_label_property_to_mgp_list(mgp_graph *graph, mgp_memory *memory, mgp_list **result, + const auto &label_property_pair) { + return WrapExceptions([graph, memory, result, &label_property_pair]() { + const auto label_id_str = std::visit( + [label_id = label_property_pair.first](const auto *impl) { return impl->LabelToName(label_id); }, graph->impl); + const auto property_id_str = std::visit( + [property_id = label_property_pair.second](const auto *impl) { return impl->PropertyToName(property_id); }, + graph->impl); + + // This is hack to avoid dealing with pairs + mgp_value *label_property = nullptr; + auto final_str = label_id_str + ":"; + final_str += property_id_str; + + if (const auto err_str = mgp_value_make_string(final_str.c_str(), memory, &label_property); + err_str != mgp_error::MGP_ERROR_NO_ERROR) { + throw std::logic_error( + "Creating a list of label+property pairs failed due to failure of creating label+property value"); + } + if (const auto err_list = mgp_list_append_extend(*result, label_property); + err_list != mgp_error::MGP_ERROR_NO_ERROR) { + throw std::logic_error( + "Creating a list of label-property pairs due to failure of appending label+property value"); + } + + mgp_value_destroy(label_property); + }); +} + +mgp_error mgp_list_all_label_property_indices(mgp_graph *graph, mgp_memory *memory, mgp_list **result) { + return WrapExceptions([graph, memory, result]() { + const auto index_res = + std::visit(memgraph::utils::Overloaded{ + [](memgraph::query::DbAccessor *impl) { return impl->ListAllIndices().label_property; }, + [](memgraph::query::SubgraphDbAccessor *impl) { + return impl->GetAccessor()->ListAllIndices().label_property; + }}, + graph->impl); + + if (const auto err = mgp_list_make_empty(index_res.size(), memory, result); err != mgp_error::MGP_ERROR_NO_ERROR) { + throw std::logic_error("Listing all label+property indices failed due to failure of creating list"); + } + + for (const auto &label_property_pair : index_res) { + if (const auto err = create_and_append_label_property_to_mgp_list(graph, memory, result, label_property_pair); + err != mgp_error::MGP_ERROR_NO_ERROR) { + throw std::logic_error( + "Listing all label+property indices failed due to failure of appending label+property value"); + } + } + }); +} + +mgp_error mgp_create_existence_constraint(mgp_graph *graph, const char *label, const char *property, int *result) { + return WrapExceptions( + [graph, label, property]() { + const auto label_id = std::visit([label](auto *impl) { return impl->NameToLabel(label); }, graph->impl); + const auto property_id = + std::visit([property](auto *impl) { return impl->NameToProperty(property); }, graph->impl); + const auto exist_res = std::visit( + memgraph::utils::Overloaded{[label_id, property_id](memgraph::query::DbAccessor *impl) { + return impl->CreateExistenceConstraint(label_id, property_id); + }, + [label_id, property_id](memgraph::query::SubgraphDbAccessor *impl) { + return impl->GetAccessor()->CreateExistenceConstraint(label_id, property_id); + }}, + graph->impl); + return exist_res.HasError() ? 0 : 1; + }, + result); +} + +mgp_error mgp_drop_existence_constraint(mgp_graph *graph, const char *label, const char *property, int *result) { + return WrapExceptions( + [graph, label, property]() { + const auto label_id = std::visit([label](auto *impl) { return impl->NameToLabel(label); }, graph->impl); + const auto property_id = + std::visit([property](auto *impl) { return impl->NameToProperty(property); }, graph->impl); + const auto exist_res = std::visit( + memgraph::utils::Overloaded{[label_id, property_id](memgraph::query::DbAccessor *impl) { + return impl->DropExistenceConstraint(label_id, property_id); + }, + [label_id, property_id](memgraph::query::SubgraphDbAccessor *impl) { + return impl->GetAccessor()->DropExistenceConstraint(label_id, property_id); + }}, + graph->impl); + return exist_res.HasError() ? 0 : 1; + }, + result); +} + +mgp_error mgp_list_all_existence_constraints(mgp_graph *graph, mgp_memory *memory, mgp_list **result) { + return WrapExceptions([graph, memory, result]() { + const auto constraint_res = + std::visit(memgraph::utils::Overloaded{ + [](memgraph::query::DbAccessor *impl) { return impl->ListAllConstraints().existence; }, + [](memgraph::query::SubgraphDbAccessor *impl) { + return impl->GetAccessor()->ListAllConstraints().existence; + }}, + graph->impl); + + if (const auto err = mgp_list_make_empty(constraint_res.size(), memory, result); + err != mgp_error::MGP_ERROR_NO_ERROR) { + throw std::logic_error("Listing all existence constraints failed due to failure of creating a list"); + } + + for (const auto &label_property_pair : constraint_res) { + if (const auto err = create_and_append_label_property_to_mgp_list(graph, memory, result, label_property_pair); + err != mgp_error::MGP_ERROR_NO_ERROR) { + throw std::logic_error( + "Listing all existence constraints failed due to failure of appending label+property value"); + } + } + }); +} + +mgp_error mgp_create_unique_constraint(mgp_graph *graph, const char *label, mgp_value *properties, int *result) { + return WrapExceptions( + [graph, label, properties]() { + const auto label_id = std::visit([label](auto *impl) { return impl->NameToLabel(label); }, graph->impl); + std::set<memgraph::storage::PropertyId> property_ids; + for (const auto &elem : properties->list_v->elems) { + property_ids.insert(std::visit( + [prop_str = elem.string_v](auto *impl) { return impl->NameToProperty(prop_str); }, graph->impl)); + } + + const auto unique_res = std::visit( + memgraph::utils::Overloaded{[label_id, property_ids](memgraph::query::DbAccessor *impl) { + return impl->CreateUniqueConstraint(label_id, property_ids); + }, + [label_id, property_ids](memgraph::query::SubgraphDbAccessor *impl) { + return impl->GetAccessor()->CreateUniqueConstraint(label_id, property_ids); + }}, + graph->impl); + return unique_res.HasError() ? 0 : 1; + }, + result); +} + +mgp_error mgp_drop_unique_constraint(mgp_graph *graph, const char *label, mgp_value *properties, int *result) { + return WrapExceptions( + [graph, label, properties]() { + const auto label_id = std::visit([label](auto *impl) { return impl->NameToLabel(label); }, graph->impl); + std::set<memgraph::storage::PropertyId> property_ids; + for (const auto &elem : properties->list_v->elems) { + property_ids.insert(std::visit( + [prop_str = elem.string_v](auto *impl) { return impl->NameToProperty(prop_str); }, graph->impl)); + } + + const auto unique_res = std::visit( + memgraph::utils::Overloaded{[label_id, property_ids](memgraph::query::DbAccessor *impl) { + return impl->DropUniqueConstraint(label_id, property_ids); + }, + [label_id, property_ids](memgraph::query::SubgraphDbAccessor *impl) { + return impl->GetAccessor()->DropUniqueConstraint(label_id, property_ids); + }}, + graph->impl); + return unique_res == memgraph::storage::UniqueConstraints::DeletionStatus::SUCCESS ? 1 : 0; + }, + result); +} + +mgp_error mgp_list_all_unique_constraints(mgp_graph *graph, mgp_memory *memory, mgp_list **result) { + return WrapExceptions([graph, memory, result]() { + const auto constraints_res = std::visit( + memgraph::utils::Overloaded{ + [](memgraph::query::DbAccessor *impl) { return impl->ListAllConstraints().unique; }, + [](memgraph::query::SubgraphDbAccessor *impl) { return impl->GetAccessor()->ListAllConstraints().unique; }}, + graph->impl); + + if (const auto err = mgp_list_make_empty(constraints_res.size(), memory, result); + err != mgp_error::MGP_ERROR_NO_ERROR) { + throw std::logic_error("Listing all unique constraints failed due to failure of creating a list"); + } + + for (const auto &label_properties_pair : constraints_res) { + const std::string label_id_str = + std::visit([label_id = label_properties_pair.first](const auto *impl) { return impl->LabelToName(label_id); }, + graph->impl); + const std::vector<std::string> properties_str = std::visit( + [property_ids = label_properties_pair.second](const auto *impl) { + std::vector<std::string> property_ids_str; + property_ids_str.reserve(property_ids.size()); + std::transform(property_ids.begin(), property_ids.end(), std::back_inserter(property_ids_str), + [impl](const auto &property_id) { return impl->PropertyToName(property_id); }); + return property_ids_str; + }, + graph->impl); + + mgp_list *label_properties_mgp_list = nullptr; + if (const auto properties_mgp_list_err = + mgp_list_make_empty(properties_str.size() + 1, memory, &label_properties_mgp_list); + properties_mgp_list_err != mgp_error::MGP_ERROR_NO_ERROR) { + throw std::logic_error("Listing all unique constraints failed due to failure of creating an inner list"); + } + + mgp_value *mgp_value_label = nullptr; + if (const auto err_label = mgp_value_make_string(label_id_str.c_str(), memory, &mgp_value_label); + err_label != mgp_error::MGP_ERROR_NO_ERROR) { + throw std::logic_error("Listing all unique constraints failed due to failure of creating a label value"); + } + if (const auto err_label_into_list = mgp_list_append_extend(label_properties_mgp_list, mgp_value_label); + err_label_into_list != mgp_error::MGP_ERROR_NO_ERROR) { + throw std::logic_error("Listing all unique constraints failed due to failure of appending a label value"); + } + + mgp_value_destroy(mgp_value_label); + + for (const std::string &property_str : properties_str) { + mgp_value *property_mgp_value = nullptr; + if (const auto err_str = mgp_value_make_string(property_str.c_str(), memory, &property_mgp_value); + err_str != mgp_error::MGP_ERROR_NO_ERROR) { + throw std::logic_error("Listing all unique constraints failed due to failure of creating a property value"); + } + if (const auto err_list = mgp_list_append_extend(label_properties_mgp_list, property_mgp_value); + err_list != mgp_error::MGP_ERROR_NO_ERROR) { + throw std::logic_error("Listing all unique constraints failed due to failure of appending a property value"); + } + mgp_value_destroy(property_mgp_value); + } + mgp_value value(label_properties_mgp_list, label_properties_mgp_list->GetMemoryResource()); + + if (const auto err_list = mgp_list_append_extend(*result, &value); err_list != mgp_error::MGP_ERROR_NO_ERROR) { + throw std::logic_error("Listing all unique constraints failed due to failure of creating label+property value"); + } + mgp_value_destroy(&value); + } + }); +} + mgp_error mgp_graph_is_mutable(mgp_graph *graph, int *result) { *result = MgpGraphIsMutable(*graph) ? 1 : 0; return mgp_error::MGP_ERROR_NO_ERROR; diff --git a/src/storage/v2/inmemory/storage.cpp b/src/storage/v2/inmemory/storage.cpp index d0d1dd071..9f00081f6 100644 --- a/src/storage/v2/inmemory/storage.cpp +++ b/src/storage/v2/inmemory/storage.cpp @@ -1003,7 +1003,7 @@ void InMemoryStorage::InMemoryAccessor::FinalizeTransaction() { } utils::BasicResult<StorageIndexDefinitionError, void> InMemoryStorage::InMemoryAccessor::CreateIndex(LabelId label) { - MG_ASSERT(unique_guard_.owns_lock(), "Create index requires a unique access to the storage!"); + MG_ASSERT(unique_guard_.owns_lock(), "Creating label index requires a unique access to the storage!"); auto *in_memory = static_cast<InMemoryStorage *>(storage_); auto *mem_label_index = static_cast<InMemoryLabelIndex *>(in_memory->indices_.label_index_.get()); if (!mem_label_index->CreateIndex(label, in_memory->vertices_.access(), std::nullopt)) { @@ -1017,7 +1017,7 @@ utils::BasicResult<StorageIndexDefinitionError, void> InMemoryStorage::InMemoryA utils::BasicResult<StorageIndexDefinitionError, void> InMemoryStorage::InMemoryAccessor::CreateIndex( LabelId label, PropertyId property) { - MG_ASSERT(unique_guard_.owns_lock(), "Create index requires a unique access to the storage!"); + MG_ASSERT(unique_guard_.owns_lock(), "Creating label-property index requires a unique access to the storage!"); auto *in_memory = static_cast<InMemoryStorage *>(storage_); auto *mem_label_property_index = static_cast<InMemoryLabelPropertyIndex *>(in_memory->indices_.label_property_index_.get()); @@ -1031,7 +1031,7 @@ utils::BasicResult<StorageIndexDefinitionError, void> InMemoryStorage::InMemoryA } utils::BasicResult<StorageIndexDefinitionError, void> InMemoryStorage::InMemoryAccessor::DropIndex(LabelId label) { - MG_ASSERT(unique_guard_.owns_lock(), "Create index requires a unique access to the storage!"); + MG_ASSERT(unique_guard_.owns_lock(), "Dropping label index requires a unique access to the storage!"); auto *in_memory = static_cast<InMemoryStorage *>(storage_); auto *mem_label_index = static_cast<InMemoryLabelIndex *>(in_memory->indices_.label_index_.get()); if (!mem_label_index->DropIndex(label)) { @@ -1045,7 +1045,7 @@ utils::BasicResult<StorageIndexDefinitionError, void> InMemoryStorage::InMemoryA utils::BasicResult<StorageIndexDefinitionError, void> InMemoryStorage::InMemoryAccessor::DropIndex( LabelId label, PropertyId property) { - MG_ASSERT(unique_guard_.owns_lock(), "Create index requires a unique access to the storage!"); + MG_ASSERT(unique_guard_.owns_lock(), "Dropping label-property index requires a unique access to the storage!"); auto *in_memory = static_cast<InMemoryStorage *>(storage_); auto *mem_label_property_index = static_cast<InMemoryLabelPropertyIndex *>(in_memory->indices_.label_property_index_.get()); @@ -1060,7 +1060,7 @@ utils::BasicResult<StorageIndexDefinitionError, void> InMemoryStorage::InMemoryA utils::BasicResult<StorageExistenceConstraintDefinitionError, void> InMemoryStorage::InMemoryAccessor::CreateExistenceConstraint(LabelId label, PropertyId property) { - MG_ASSERT(unique_guard_.owns_lock(), "Create index requires a unique access to the storage!"); + MG_ASSERT(unique_guard_.owns_lock(), "Creating existence requires a unique access to the storage!"); auto *in_memory = static_cast<InMemoryStorage *>(storage_); auto *existence_constraints = in_memory->constraints_.existence_constraints_.get(); if (existence_constraints->ConstraintExists(label, property)) { @@ -1078,7 +1078,7 @@ InMemoryStorage::InMemoryAccessor::CreateExistenceConstraint(LabelId label, Prop utils::BasicResult<StorageExistenceConstraintDroppingError, void> InMemoryStorage::InMemoryAccessor::DropExistenceConstraint(LabelId label, PropertyId property) { - MG_ASSERT(unique_guard_.owns_lock(), "Create index requires a unique access to the storage!"); + MG_ASSERT(unique_guard_.owns_lock(), "Dropping existence constraint requires a unique access to the storage!"); auto *in_memory = static_cast<InMemoryStorage *>(storage_); auto *existence_constraints = in_memory->constraints_.existence_constraints_.get(); if (!existence_constraints->DropConstraint(label, property)) { @@ -1090,7 +1090,7 @@ InMemoryStorage::InMemoryAccessor::DropExistenceConstraint(LabelId label, Proper utils::BasicResult<StorageUniqueConstraintDefinitionError, UniqueConstraints::CreationStatus> InMemoryStorage::InMemoryAccessor::CreateUniqueConstraint(LabelId label, const std::set<PropertyId> &properties) { - MG_ASSERT(unique_guard_.owns_lock(), "Create index requires a unique access to the storage!"); + MG_ASSERT(unique_guard_.owns_lock(), "Creating unique constraint requires a unique access to the storage!"); auto *in_memory = static_cast<InMemoryStorage *>(storage_); auto *mem_unique_constraints = static_cast<InMemoryUniqueConstraints *>(in_memory->constraints_.unique_constraints_.get()); @@ -1107,7 +1107,7 @@ InMemoryStorage::InMemoryAccessor::CreateUniqueConstraint(LabelId label, const s UniqueConstraints::DeletionStatus InMemoryStorage::InMemoryAccessor::DropUniqueConstraint( LabelId label, const std::set<PropertyId> &properties) { - MG_ASSERT(unique_guard_.owns_lock(), "Create index requires a unique access to the storage!"); + MG_ASSERT(unique_guard_.owns_lock(), "Dropping unique constraint requires a unique access to the storage!"); auto *in_memory = static_cast<InMemoryStorage *>(storage_); auto *mem_unique_constraints = static_cast<InMemoryUniqueConstraints *>(in_memory->constraints_.unique_constraints_.get()); diff --git a/tests/e2e/query_modules/schema_test.py b/tests/e2e/query_modules/schema_test.py index e61d9df1b..515514a74 100644 --- a/tests/e2e/query_modules/schema_test.py +++ b/tests/e2e/query_modules/schema_test.py @@ -15,6 +15,410 @@ import pytest from common import connect, execute_and_fetch_all +def test_assert_creates_label_index_empty_list(): + cursor = connect().cursor() + results = list( + execute_and_fetch_all( + cursor, + "CALL libschema.assert({Person: []}, {}) YIELD * RETURN *;", + ) + ) + assert results == [("Created", "", [], "Person", False)] + show_index_results = list(execute_and_fetch_all(cursor, "SHOW INDEX INFO;")) + assert show_index_results == [("label", "Person", None, 0)] + execute_and_fetch_all(cursor, "DROP INDEX ON :Person;") + + +def test_assert_creates_label_index_empty_string(): + cursor = connect().cursor() + results = list( + execute_and_fetch_all( + cursor, + "CALL libschema.assert({Person: ['']}, {}) YIELD * RETURN *;", + ) + ) + assert results == [("Created", "", [], "Person", False)] + show_index_results = list(execute_and_fetch_all(cursor, "SHOW INDEX INFO;")) + assert show_index_results == [("label", "Person", None, 0)] + execute_and_fetch_all(cursor, "DROP INDEX ON :Person;") + + +def test_assert_index_wrong_properties_type(): + cursor = connect().cursor() + execute_and_fetch_all( + cursor, + "CALL libschema.assert({Person: ''}, {}) YIELD * RETURN *;", + ) + assert list(execute_and_fetch_all(cursor, "SHOW INDEX INFO;")) == [] + + +def test_assert_property_is_not_a_string(): + cursor = connect().cursor() + execute_and_fetch_all( + cursor, + "CALL libschema.assert({Person: ['name', 1]}, {}) YIELD * RETURN *;", + ) + show_index_results = list(execute_and_fetch_all(cursor, "SHOW INDEX INFO;")) + assert show_index_results == [("label+property", "Person", "name", 0)] + execute_and_fetch_all(cursor, "DROP INDEX ON :Person(name);") + + +def test_assert_creates_label_index_multiple_empty_strings(): + cursor = connect().cursor() + results = list( + execute_and_fetch_all( + cursor, + "CALL libschema.assert({Person: ['', '', '', '']}, {}) YIELD * RETURN *;", + ) + ) + assert results == [("Created", "", [], "Person", False)] + show_index_results = list(execute_and_fetch_all(cursor, "SHOW INDEX INFO;")) + assert show_index_results == [("label", "Person", None, 0)] + execute_and_fetch_all(cursor, "DROP INDEX ON :Person;") + + +def test_assert_creates_label_property_index(): + cursor = connect().cursor() + results = list( + execute_and_fetch_all( + cursor, + "CALL libschema.assert({Person: ['name']}, {}) YIELD * RETURN *;", + ) + ) + assert results == [("Created", "name", ["name"], "Person", False)] + show_index_results = list(execute_and_fetch_all(cursor, "SHOW INDEX INFO;")) + assert show_index_results == [("label+property", "Person", "name", 0)] + execute_and_fetch_all(cursor, "DROP INDEX ON :Person(name);") + + +def test_assert_creates_multiple_indices(): + cursor = connect().cursor() + results = list( + execute_and_fetch_all( + cursor, + "CALL libschema.assert({Person: ['', 'id', 'name'], Ball: ['', 'size', 'size', '']}, {}) YIELD * RETURN *;", + ) + ) + assert len(results) == 5 + assert results[0] == ("Created", "", [], "Ball", False) + assert results[1] == ("Created", "size", ["size"], "Ball", False) + assert results[2] == ("Created", "", [], "Person", False) + assert results[3] == ("Created", "id", ["id"], "Person", False) + assert results[4] == ("Created", "name", ["name"], "Person", False) + show_index_results = list(execute_and_fetch_all(cursor, "SHOW INDEX INFO;")) + assert len(show_index_results) == 5 + assert show_index_results[0] == ("label", "Ball", None, 0) + assert show_index_results[1] == ("label", "Person", None, 0) + assert show_index_results[2] == ("label+property", "Ball", "size", 0) + assert show_index_results[3] == ("label+property", "Person", "id", 0) + assert show_index_results[4] == ("label+property", "Person", "name", 0) + execute_and_fetch_all(cursor, "DROP INDEX ON :Person;") + execute_and_fetch_all(cursor, "DROP INDEX ON :Person(id);") + execute_and_fetch_all(cursor, "DROP INDEX ON :Person(name);") + execute_and_fetch_all(cursor, "DROP INDEX ON :Ball;") + execute_and_fetch_all(cursor, "DROP INDEX ON :Ball(size);") + + +def test_assert_creates_existence_constraints(): + cursor = connect().cursor() + results = list( + execute_and_fetch_all( + cursor, + "CALL libschema.assert({}, {}, {Person: ['name', 'surname']}) YIELD * RETURN *;", + ) + ) + assert results == [ + ("Created", "name", ["name"], "Person", False), + ("Created", "surname", ["surname"], "Person", False), + ] + assert list(execute_and_fetch_all(cursor, "SHOW INDEX INFO;")) == [] + show_constraint_results = list(execute_and_fetch_all(cursor, "SHOW CONSTRAINT INFO;")) + assert show_constraint_results == [("exists", "Person", "name"), ("exists", "Person", "surname")] + execute_and_fetch_all(cursor, "DROP CONSTRAINT ON (n:Person) ASSERT EXISTS (n.name);") + execute_and_fetch_all(cursor, "DROP CONSTRAINT ON (n:Person) ASSERT EXISTS (n.surname);") + + +def test_assert_dropping_indices(): + cursor = connect().cursor() + execute_and_fetch_all(cursor, "CREATE INDEX ON :Person(name);") + execute_and_fetch_all(cursor, "CREATE INDEX ON :Person(id);") + execute_and_fetch_all(cursor, "CREATE INDEX ON :Ball(size);") + execute_and_fetch_all(cursor, "CREATE INDEX ON :Ball;") + results = list(execute_and_fetch_all(cursor, "CALL libschema.assert({}, {}) YIELD * RETURN *;")) + assert len(results) == 4 + assert results[0] == ("Dropped", "", [], "Ball", False) + assert results[1] == ("Dropped", "size", ["size"], "Ball", False) + assert results[2] == ("Dropped", "id", ["id"], "Person", False) + assert results[3] == ("Dropped", "name", ["name"], "Person", False) + show_index_results = list(execute_and_fetch_all(cursor, "SHOW INDEX INFO;")) + assert show_index_results == [] + + +def test_assert_existence_constraint_properties_not_list(): + cursor = connect().cursor() + execute_and_fetch_all(cursor, "CALL libschema.assert({}, {}, {Person: 'name'}) YIELD * RETURN *;") + assert list(execute_and_fetch_all(cursor, "SHOW CONSTRAINT INFO;")) == [] + + +def test_assert_existence_constraint_property_not_string(): + cursor = connect().cursor() + execute_and_fetch_all(cursor, "CALL libschema.assert({}, {}, {Person: ['name', 1]}) YIELD * RETURN *;") + show_constraint_results = list(execute_and_fetch_all(cursor, "SHOW CONSTRAINT INFO;")) + assert show_constraint_results == [("exists", "Person", "name")] + execute_and_fetch_all(cursor, "DROP CONSTRAINT ON (n:Person) ASSERT EXISTS (n.name);") + + +def test_assert_existence_constraint_property_empty_string(): + cursor = connect().cursor() + execute_and_fetch_all(cursor, "CALL libschema.assert({}, {}, {Person: ['']}) YIELD * RETURN *;") + assert list(execute_and_fetch_all(cursor, "SHOW CONSTRAINT INFO;")) == [] + + +def test_assert_creates_indices_and_existence_constraints(): + cursor = connect().cursor() + results = list( + execute_and_fetch_all( + cursor, + "CALL libschema.assert({Person: ['', 'id']}, {}, {Person: ['name', 'surname']}) YIELD * RETURN *;", + ) + ) + assert len(results) == 4 + assert results[0] == ("Created", "", [], "Person", False) + assert results[1] == ("Created", "id", ["id"], "Person", False) + assert results[2] == ("Created", "name", ["name"], "Person", False) + assert results[3] == ("Created", "surname", ["surname"], "Person", False) + show_index_results = list(execute_and_fetch_all(cursor, "SHOW INDEX INFO;")) + assert show_index_results == [("label", "Person", None, 0), ("label+property", "Person", "id", 0)] + show_constraint_results = list(execute_and_fetch_all(cursor, "SHOW CONSTRAINT INFO;")) + assert show_constraint_results == [("exists", "Person", "name"), ("exists", "Person", "surname")] + execute_and_fetch_all(cursor, "DROP INDEX ON :Person;") + execute_and_fetch_all(cursor, "DROP INDEX ON :Person(id);") + execute_and_fetch_all(cursor, "DROP CONSTRAINT ON (n:Person) ASSERT EXISTS (n.name);") + execute_and_fetch_all(cursor, "DROP CONSTRAINT ON (n:Person) ASSERT EXISTS (n.surname);") + + +def test_assert_drops_existence_constraints(): + cursor = connect().cursor() + execute_and_fetch_all(cursor, "CREATE CONSTRAINT ON (n:Person) ASSERT EXISTS (n.name);") + execute_and_fetch_all(cursor, "CREATE CONSTRAINT ON (n:Person) ASSERT EXISTS (n.surname);") + results = list(execute_and_fetch_all(cursor, "CALL libschema.assert({}, {}, {}) YIELD * RETURN *;")) + assert len(results) == 2 + assert results[0] == ("Dropped", "name", ["name"], "Person", False) + assert results[1] == ("Dropped", "surname", ["surname"], "Person", False) + show_constraint_results = list(execute_and_fetch_all(cursor, "SHOW CONSTRAINT INFO;")) + assert show_constraint_results == [] + + +def test_assert_creates_unique_constraints(): + cursor = connect().cursor() + results = list( + execute_and_fetch_all( + cursor, + "CALL libschema.assert({}, {Person: [['name', 'surname']]}) YIELD * RETURN *;", + ) + ) + assert results == [("Created", "[name, surname]", ["name", "surname"], "Person", True)] + assert list(execute_and_fetch_all(cursor, "SHOW INDEX INFO;")) == [] + show_constraint_results = list(execute_and_fetch_all(cursor, "SHOW CONSTRAINT INFO;")) + assert show_constraint_results == [("unique", "Person", ["name", "surname"])] + execute_and_fetch_all(cursor, "DROP CONSTRAINT ON (n:Person) ASSERT n.name, n.surname IS UNIQUE;") + + +def test_assert_creates_multiple_unique_constraints(): + cursor = connect().cursor() + results = list( + execute_and_fetch_all( + cursor, + "CALL libschema.assert({}, {Person: [['name', 'surname'], ['id']]}) YIELD * RETURN *;", + ) + ) + assert results == [ + ("Created", "[name, surname]", ["name", "surname"], "Person", True), + ("Created", "[id]", ["id"], "Person", True), + ] + assert list(execute_and_fetch_all(cursor, "SHOW INDEX INFO;")) == [] + show_constraint_results = list(execute_and_fetch_all(cursor, "SHOW CONSTRAINT INFO;")) + assert show_constraint_results == [("unique", "Person", ["name", "surname"]), ("unique", "Person", ["id"])] + execute_and_fetch_all(cursor, "DROP CONSTRAINT ON (n:Person) ASSERT n.name, n.surname IS UNIQUE;") + execute_and_fetch_all(cursor, "DROP CONSTRAINT ON (n:Person) ASSERT n.id IS UNIQUE;") + + +def test_assert_creates_unique_constraints_skip_invalid(): + cursor = connect().cursor() + results = list( + execute_and_fetch_all( + cursor, + "CALL libschema.assert({}, {Person: [['name', 'surname'], 'wrong_type']}) YIELD * RETURN *;", + ) + ) + assert results == [("Created", "[name, surname]", ["name", "surname"], "Person", True)] + assert list(execute_and_fetch_all(cursor, "SHOW INDEX INFO;")) == [] + show_constraint_results = list(execute_and_fetch_all(cursor, "SHOW CONSTRAINT INFO;")) + assert show_constraint_results == [("unique", "Person", ["name", "surname"])] + execute_and_fetch_all(cursor, "DROP CONSTRAINT ON (n:Person) ASSERT n.name, n.surname IS UNIQUE;") + + +def test_assert_creates_unique_constraints_skip_invalid_map_type(): + cursor = connect().cursor() + results = list( + execute_and_fetch_all( + cursor, + "CALL libschema.assert({}, {Person: [['name', 'surname']], Ball: 'wrong_type'}) YIELD * RETURN *;", + ) + ) + assert results == [("Created", "[name, surname]", ["name", "surname"], "Person", True)] + assert list(execute_and_fetch_all(cursor, "SHOW INDEX INFO;")) == [] + show_constraint_results = list(execute_and_fetch_all(cursor, "SHOW CONSTRAINT INFO;")) + assert show_constraint_results == [("unique", "Person", ["name", "surname"])] + execute_and_fetch_all(cursor, "DROP CONSTRAINT ON (n:Person) ASSERT n.name, n.surname IS UNIQUE;") + + +def test_assert_creates_constraints_and_indices(): + cursor = connect().cursor() + results = list( + execute_and_fetch_all( + cursor, + "CALL libschema.assert({Person: ['', 'id']}, {Person: [['name', 'surname'], ['id']]}, {Person: ['name', 'surname']}) YIELD * RETURN *;", + ) + ) + assert len(results) == 6 + assert results[0] == ("Created", "", [], "Person", False) + assert results[1] == ("Created", "id", ["id"], "Person", False) + assert results[2] == ("Created", "name", ["name"], "Person", False) + assert results[3] == ("Created", "surname", ["surname"], "Person", False) + assert results[4] == ("Created", "[name, surname]", ["name", "surname"], "Person", True) + assert results[5] == ("Created", "[id]", ["id"], "Person", True) + show_index_results = list(execute_and_fetch_all(cursor, "SHOW INDEX INFO;")) + assert show_index_results == [("label", "Person", None, 0), ("label+property", "Person", "id", 0)] + show_constraint_results = list(execute_and_fetch_all(cursor, "SHOW CONSTRAINT INFO;")) + assert show_constraint_results == [ + ("exists", "Person", "name"), + ("exists", "Person", "surname"), + ("unique", "Person", ["name", "surname"]), + ("unique", "Person", ["id"]), + ] + execute_and_fetch_all(cursor, "DROP CONSTRAINT ON (n:Person) ASSERT n.name, n.surname IS UNIQUE;") + execute_and_fetch_all(cursor, "DROP CONSTRAINT ON (n:Person) ASSERT n.id IS UNIQUE;") + execute_and_fetch_all(cursor, "DROP INDEX ON :Person;") + execute_and_fetch_all(cursor, "DROP INDEX ON :Person(id);") + execute_and_fetch_all(cursor, "DROP CONSTRAINT ON (n:Person) ASSERT EXISTS (n.name);") + execute_and_fetch_all(cursor, "DROP CONSTRAINT ON (n:Person) ASSERT EXISTS (n.surname);") + + +def test_assert_drops_unique_constraints(): + cursor = connect().cursor() + execute_and_fetch_all(cursor, "CREATE CONSTRAINT ON (n:Person) ASSERT n.name, n.surname IS UNIQUE;") + execute_and_fetch_all(cursor, "CREATE CONSTRAINT ON (n:Person) ASSERT n.id IS UNIQUE;") + results = list(execute_and_fetch_all(cursor, "CALL libschema.assert({}, {}, {}) YIELD * RETURN *;")) + assert len(results) == 2 + assert results[0] == ("Dropped", "[id]", ["id"], "Person", True) + assert results[1] == ("Dropped", "[name, surname]", ["name", "surname"], "Person", True) + show_constraint_results = list(execute_and_fetch_all(cursor, "SHOW CONSTRAINT INFO;")) + assert show_constraint_results == [] + + +def test_assert_drops_indices_and_constraints(): + cursor = connect().cursor() + execute_and_fetch_all(cursor, "CREATE INDEX ON :Person;") + execute_and_fetch_all(cursor, "CREATE INDEX ON :Person(id);") + execute_and_fetch_all(cursor, "CREATE CONSTRAINT ON (n:Person) ASSERT n.name, n.surname IS UNIQUE;") + execute_and_fetch_all(cursor, "CREATE CONSTRAINT ON (n:Person) ASSERT n.id IS UNIQUE;") + execute_and_fetch_all(cursor, "CREATE CONSTRAINT ON (n:Person) ASSERT EXISTS (n.name);") + execute_and_fetch_all(cursor, "CREATE CONSTRAINT ON (n:Person) ASSERT EXISTS (n.surname);") + results = list(execute_and_fetch_all(cursor, "CALL libschema.assert({}, {}, {}) YIELD * RETURN *;")) + assert len(results) == 6 + assert results[0] == ("Dropped", "", [], "Person", False) + assert results[1] == ("Dropped", "id", ["id"], "Person", False) + assert results[2] == ("Dropped", "name", ["name"], "Person", False) + assert results[3] == ("Dropped", "surname", ["surname"], "Person", False) + assert results[4] == ("Dropped", "[id]", ["id"], "Person", True) + assert results[5] == ("Dropped", "[name, surname]", ["name", "surname"], "Person", True) + show_index_results = list(execute_and_fetch_all(cursor, "SHOW INDEX INFO;")) + assert show_index_results == [] + show_constraint_results = list(execute_and_fetch_all(cursor, "SHOW CONSTRAINT INFO;")) + assert show_constraint_results == [] + + +def test_assert_does_not_drop_indices_and_constraints(): + cursor = connect().cursor() + execute_and_fetch_all(cursor, "CREATE INDEX ON :Person;") + execute_and_fetch_all(cursor, "CREATE INDEX ON :Person(id);") + execute_and_fetch_all(cursor, "CREATE CONSTRAINT ON (n:Person) ASSERT n.name, n.surname IS UNIQUE;") + execute_and_fetch_all(cursor, "CREATE CONSTRAINT ON (n:Person) ASSERT n.id IS UNIQUE;") + execute_and_fetch_all(cursor, "CREATE CONSTRAINT ON (n:Person) ASSERT EXISTS (n.name);") + execute_and_fetch_all(cursor, "CREATE CONSTRAINT ON (n:Person) ASSERT EXISTS (n.surname);") + results = list(execute_and_fetch_all(cursor, "CALL libschema.assert({}, {}, {}, false) YIELD * RETURN *;")) + assert len(results) == 0 + show_index_results = list(execute_and_fetch_all(cursor, "SHOW INDEX INFO;")) + assert show_index_results == [("label", "Person", None, 0), ("label+property", "Person", "id", 0)] + show_constraint_results = list(execute_and_fetch_all(cursor, "SHOW CONSTRAINT INFO;")) + assert show_constraint_results == [ + ("exists", "Person", "name"), + ("exists", "Person", "surname"), + ("unique", "Person", ["name", "surname"]), + ("unique", "Person", ["id"]), + ] + execute_and_fetch_all(cursor, "DROP INDEX ON :Person;") + execute_and_fetch_all(cursor, "DROP INDEX ON :Person(id);") + execute_and_fetch_all(cursor, "DROP CONSTRAINT ON (n:Person) ASSERT n.name, n.surname IS UNIQUE;") + execute_and_fetch_all(cursor, "DROP CONSTRAINT ON (n:Person) ASSERT n.id IS UNIQUE;") + execute_and_fetch_all(cursor, "DROP CONSTRAINT ON (n:Person) ASSERT EXISTS (n.name);") + execute_and_fetch_all(cursor, "DROP CONSTRAINT ON (n:Person) ASSERT EXISTS (n.surname);") + + +def test_assert_keeps_existing_indices_and_constraints(): + cursor = connect().cursor() + assert list(execute_and_fetch_all(cursor, "SHOW INDEX INFO;")) == [] + assert list(execute_and_fetch_all(cursor, "SHOW CONSTRAINT INFO;")) == [] + execute_and_fetch_all(cursor, "CREATE INDEX ON :Person;") + execute_and_fetch_all(cursor, "CREATE INDEX ON :Person(id);") + execute_and_fetch_all(cursor, "CREATE CONSTRAINT ON (n:Person) ASSERT n.name, n.surname IS UNIQUE;") + execute_and_fetch_all(cursor, "CREATE CONSTRAINT ON (n:Person) ASSERT n.id IS UNIQUE;") + execute_and_fetch_all(cursor, "CREATE CONSTRAINT ON (n:Person) ASSERT EXISTS (n.name);") + execute_and_fetch_all(cursor, "CREATE CONSTRAINT ON (n:Person) ASSERT EXISTS (n.surname);") + results = list( + execute_and_fetch_all( + cursor, + "CALL libschema.assert({Person: ['id']}, {Person: [['name', 'surname']]}, {Person: ['name']}) YIELD * RETURN *;", + ) + ) + + print(results) + + assert len(results) == 6 + + assert results[0] == ("Kept", "id", ["id"], "Person", False) # label+property index on Person(id) should be kept + assert results[1] == ("Dropped", "", [], "Person", False) # label index on Person should be deleted + assert results[2] == ( + "Kept", + "name", + ["name"], + "Person", + False, + ) # existence constraint on Person(name) should be kept + assert results[3] == ( + "Dropped", + "surname", + ["surname"], + "Person", + False, + ) # existence constraint on surname should be deleted + assert results[4] == ( + "Kept", + "[name, surname]", + ["name", "surname"], + "Person", + True, + ) # unique constraint on Person(name, surname) should be kept + assert results[5] == ( + "Dropped", + "[id]", + ["id"], + "Person", + True, + ) # unique constraint on Person(id) should be deleted + + def test_node_type_properties1(): cursor = connect().cursor() execute_and_fetch_all( diff --git a/tests/e2e/query_modules/workloads.yaml b/tests/e2e/query_modules/workloads.yaml index ae6c45c90..54641c3d4 100644 --- a/tests/e2e/query_modules/workloads.yaml +++ b/tests/e2e/query_modules/workloads.yaml @@ -27,7 +27,7 @@ workloads: args: ["query_modules/mgps_test.py"] <<: *in_memory_cluster - - name: "Convert query module test" + - name: "Schema test" pre_set_workload: "tests/e2e/x.sh" binary: "tests/e2e/pytest_runner.sh" proc: "query_modules/"