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> &param
 }
 }  // 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/"