diff --git a/src/storage/v2/durability.cpp b/src/storage/v2/durability.cpp
index 1767dbb35..9b0d38642 100644
--- a/src/storage/v2/durability.cpp
+++ b/src/storage/v2/durability.cpp
@@ -323,6 +323,8 @@ std::optional<PropertyValue> Decoder::ReadPropertyValue() {
     case Marker::DELTA_LABEL_PROPERTY_INDEX_DROP:
     case Marker::DELTA_EXISTENCE_CONSTRAINT_CREATE:
     case Marker::DELTA_EXISTENCE_CONSTRAINT_DROP:
+    case Marker::DELTA_UNIQUE_CONSTRAINT_CREATE:
+    case Marker::DELTA_UNIQUE_CONSTRAINT_DROP:
     case Marker::VALUE_FALSE:
     case Marker::VALUE_TRUE:
       return std::nullopt;
@@ -416,6 +418,8 @@ bool Decoder::SkipPropertyValue() {
     case Marker::DELTA_LABEL_PROPERTY_INDEX_DROP:
     case Marker::DELTA_EXISTENCE_CONSTRAINT_CREATE:
     case Marker::DELTA_EXISTENCE_CONSTRAINT_DROP:
+    case Marker::DELTA_UNIQUE_CONSTRAINT_CREATE:
+    case Marker::DELTA_UNIQUE_CONSTRAINT_DROP:
     case Marker::VALUE_FALSE:
     case Marker::VALUE_TRUE:
       return false;
@@ -438,7 +442,10 @@ namespace {
 // The current version of snapshot and WAL encoding / decoding.
 // IMPORTANT: Please bump this version for every snapshot and/or WAL format
 // change!!!
-const uint64_t kVersion{12};
+const uint64_t kVersion{13};
+
+const uint64_t kOldestSupportedVersion{12};
+const uint64_t kUniqueConstraintVersion{13};
 
 // Snapshot format:
 //
@@ -484,6 +491,9 @@ const uint64_t kVersion{12};
 //     * existence constraints
 //         * label
 //         * property
+//     * unique constraints (from version 13)
+//         * label
+//         * properties
 //
 // 8) Name to ID mapper data
 //     * id to name mappings
@@ -542,6 +552,9 @@ const uint64_t kVersion{12};
 //           existence constraint create, existence constraint drop
 //              * label name
 //              * property name
+//         * unique constraint create, unique constraint drop
+//              * label name
+//              * property names
 
 // This is the prefix used for Snapshot and WAL filenames. It is a timestamp
 // format that equals to: YYYYmmddHHMMSSffffff
@@ -583,6 +596,10 @@ Marker OperationToMarker(StorageGlobalOperation operation) {
       return Marker::DELTA_EXISTENCE_CONSTRAINT_CREATE;
     case StorageGlobalOperation::EXISTENCE_CONSTRAINT_DROP:
       return Marker::DELTA_EXISTENCE_CONSTRAINT_DROP;
+    case StorageGlobalOperation::UNIQUE_CONSTRAINT_CREATE:
+      return Marker::DELTA_UNIQUE_CONSTRAINT_CREATE;
+    case StorageGlobalOperation::UNIQUE_CONSTRAINT_DROP:
+      return Marker::DELTA_UNIQUE_CONSTRAINT_DROP;
   }
 }
 
@@ -647,6 +664,10 @@ WalDeltaData::Type MarkerToWalDeltaDataType(Marker marker) {
       return WalDeltaData::Type::EXISTENCE_CONSTRAINT_CREATE;
     case Marker::DELTA_EXISTENCE_CONSTRAINT_DROP:
       return WalDeltaData::Type::EXISTENCE_CONSTRAINT_DROP;
+    case Marker::DELTA_UNIQUE_CONSTRAINT_CREATE:
+      return WalDeltaData::Type::UNIQUE_CONSTRAINT_CREATE;
+    case Marker::DELTA_UNIQUE_CONSTRAINT_DROP:
+      return WalDeltaData::Type::UNIQUE_CONSTRAINT_DROP;
 
     case Marker::TYPE_NULL:
     case Marker::TYPE_BOOL:
@@ -697,6 +718,8 @@ bool IsWalDeltaDataTypeTransactionEnd(WalDeltaData::Type type) {
     case WalDeltaData::Type::LABEL_PROPERTY_INDEX_DROP:
     case WalDeltaData::Type::EXISTENCE_CONSTRAINT_CREATE:
     case WalDeltaData::Type::EXISTENCE_CONSTRAINT_DROP:
+    case WalDeltaData::Type::UNIQUE_CONSTRAINT_CREATE:
+    case WalDeltaData::Type::UNIQUE_CONSTRAINT_DROP:
       return true;
   }
 }
@@ -805,6 +828,29 @@ WalDeltaData ReadSkipWalDeltaData(Decoder *wal) {
       }
       break;
     }
+    case WalDeltaData::Type::UNIQUE_CONSTRAINT_CREATE:
+    case WalDeltaData::Type::UNIQUE_CONSTRAINT_DROP: {
+      if constexpr (read_data) {
+        auto label = wal->ReadString();
+        if (!label) throw RecoveryFailure("Invalid WAL data!");
+        delta.operation_label_properties.label = std::move(*label);
+        auto properties_count = wal->ReadUint();
+        if (!properties_count) throw RecoveryFailure("Invalid WAL data!");
+        for (uint64_t i = 0; i < *properties_count; ++i) {
+          auto property = wal->ReadString();
+          if (!property) throw RecoveryFailure("Invalid WAL data!");
+          delta.operation_label_properties.properties.emplace(
+              std::move(*property));
+        }
+      } else {
+        if (!wal->SkipString()) throw RecoveryFailure("Invalid WAL data!");
+        auto properties_count = wal->ReadUint();
+        if (!properties_count) throw RecoveryFailure("Invalid WAL data!");
+        for (uint64_t i = 0; i < *properties_count; ++i) {
+          if (!wal->SkipString()) throw RecoveryFailure("Invalid WAL data!");
+        }
+      }
+    }
   }
 
   return delta;
@@ -835,6 +881,10 @@ void RemoveRecoveredIndexConstraint(std::vector<TObj> *list, TObj obj,
     throw RecoveryFailure(error_message);
   }
 }
+
+bool IsVersionSupported(uint64_t version) {
+  return version >= kOldestSupportedVersion && version <= kVersion;
+}
 }  // namespace
 
 // Function used to read information about the snapshot file.
@@ -844,7 +894,8 @@ SnapshotInfo ReadSnapshotInfo(const std::filesystem::path &path) {
   auto version = snapshot.Initialize(path, kSnapshotMagic);
   if (!version)
     throw RecoveryFailure("Couldn't read snapshot magic and/or version!");
-  if (*version != kVersion) throw RecoveryFailure("Invalid snapshot version!");
+  if (!IsVersionSupported(*version))
+    throw RecoveryFailure("Invalid snapshot version!");
 
   // Prepare return value.
   SnapshotInfo info;
@@ -912,7 +963,8 @@ WalInfo ReadWalInfo(const std::filesystem::path &path) {
   auto version = wal.Initialize(path, kWalMagic);
   if (!version)
     throw RecoveryFailure("Couldn't read WAL magic and/or version!");
-  if (*version != kVersion) throw RecoveryFailure("Invalid WAL version!");
+  if (!IsVersionSupported(*version))
+    throw RecoveryFailure("Invalid WAL version!");
 
   // Prepare return value.
   WalInfo info;
@@ -1045,6 +1097,12 @@ bool operator==(const WalDeltaData &a, const WalDeltaData &b) {
                  b.operation_label_property.label &&
              a.operation_label_property.property ==
                  b.operation_label_property.property;
+    case WalDeltaData::Type::UNIQUE_CONSTRAINT_CREATE:
+    case WalDeltaData::Type::UNIQUE_CONSTRAINT_DROP:
+      return a.operation_label_properties.label ==
+                 b.operation_label_properties.label &&
+             a.operation_label_properties.properties ==
+                 b.operation_label_properties.properties;
   }
 }
 bool operator!=(const WalDeltaData &a, const WalDeltaData &b) {
@@ -1063,14 +1121,14 @@ uint64_t ReadWalDeltaHeader(Decoder *wal) {
   return *timestamp;
 }
 
-// Function used to either read the current WAL delta data. The WAL delta header
-// must be read before calling this function.
+// Function used to read the current WAL delta data. The WAL delta header must
+// be read before calling this function.
 WalDeltaData ReadWalDeltaData(Decoder *wal) {
   return ReadSkipWalDeltaData<true>(wal);
 }
 
-// Function used to either skip the current WAL delta data. The WAL delta header
-// must be read before calling this function.
+// Function used to skip the current WAL delta data. The WAL delta header must
+// be read before calling this function.
 WalDeltaData::Type SkipWalDeltaData(Decoder *wal) {
   auto delta = ReadSkipWalDeltaData<false>(wal);
   return delta.type;
@@ -1245,13 +1303,14 @@ void WalFile::AppendTransactionEnd(uint64_t timestamp) {
 }
 
 void WalFile::AppendOperation(StorageGlobalOperation operation, LabelId label,
-                              std::optional<PropertyId> property,
+                              const std::set<PropertyId> &properties,
                               uint64_t timestamp) {
   wal_.WriteMarker(Marker::SECTION_DELTA);
   wal_.WriteUint(timestamp);
   switch (operation) {
     case StorageGlobalOperation::LABEL_INDEX_CREATE:
     case StorageGlobalOperation::LABEL_INDEX_DROP: {
+      CHECK(properties.empty()) << "Invalid function call!";
       wal_.WriteMarker(OperationToMarker(operation));
       wal_.WriteString(name_id_mapper_->IdToName(label.AsUint()));
       break;
@@ -1260,10 +1319,22 @@ void WalFile::AppendOperation(StorageGlobalOperation operation, LabelId label,
     case StorageGlobalOperation::LABEL_PROPERTY_INDEX_DROP:
     case StorageGlobalOperation::EXISTENCE_CONSTRAINT_CREATE:
     case StorageGlobalOperation::EXISTENCE_CONSTRAINT_DROP: {
-      CHECK(property) << "Invalid function call!";
+      CHECK(properties.size() == 1) << "Invalid function call!";
       wal_.WriteMarker(OperationToMarker(operation));
       wal_.WriteString(name_id_mapper_->IdToName(label.AsUint()));
-      wal_.WriteString(name_id_mapper_->IdToName(property->AsUint()));
+      wal_.WriteString(
+          name_id_mapper_->IdToName((*properties.begin()).AsUint()));
+      break;
+    }
+    case StorageGlobalOperation::UNIQUE_CONSTRAINT_CREATE:
+    case StorageGlobalOperation::UNIQUE_CONSTRAINT_DROP: {
+      CHECK(!properties.empty()) << "Invalid function call!";
+      wal_.WriteMarker(OperationToMarker(operation));
+      wal_.WriteString(name_id_mapper_->IdToName(label.AsUint()));
+      wal_.WriteUint(properties.size());
+      for (const auto &property : properties) {
+        wal_.WriteString(name_id_mapper_->IdToName(property.AsUint()));
+      }
       break;
     }
   }
@@ -1530,10 +1601,10 @@ void Durability::AppendToWal(const Transaction &transaction,
 }
 
 void Durability::AppendToWal(StorageGlobalOperation operation, LabelId label,
-                             std::optional<PropertyId> property,
+                             const std::set<PropertyId> &properties,
                              uint64_t final_commit_timestamp) {
   if (!InitializeWalFile()) return;
-  wal_file_->AppendOperation(operation, label, property,
+  wal_file_->AppendOperation(operation, label, properties,
                              final_commit_timestamp);
   FinalizeWalFile();
 }
@@ -1742,6 +1813,19 @@ void Durability::CreateSnapshot(Transaction *transaction) {
         write_mapping(item.second);
       }
     }
+
+    // Write unique constraints.
+    {
+      auto unique = constraints_->unique_constraints.ListConstraints();
+      snapshot.WriteUint(unique.size());
+      for (const auto &item : unique) {
+        write_mapping(item.first);
+        snapshot.WriteUint(item.second.size());
+        for (const auto &property : item.second) {
+          write_mapping(property);
+        }
+      }
+    }
   }
 
   // Write mapper data.
@@ -1873,7 +1957,9 @@ void Durability::CreateSnapshot(Transaction *transaction) {
 }
 
 std::optional<Durability::RecoveryInfo> Durability::RecoverData() {
-  if (!utils::DirExists(snapshot_directory_)) return std::nullopt;
+  if (!utils::DirExists(snapshot_directory_) &&
+      !utils::DirExists(wal_directory_))
+    return std::nullopt;
 
   // Helper lambda used to recover all discovered indices and constraints. The
   // indices and constraints must be recovered after the data recovery is done
@@ -1901,23 +1987,34 @@ std::optional<Durability::RecoveryInfo> Durability::RecoverData() {
       if (ret.HasError() || !ret.GetValue())
         throw RecoveryFailure("The existence constraint must be created here!");
     }
+
+    // Recover unique constraints.
+    for (const auto &item : indices_constraints.constraints.unique) {
+      auto ret = constraints_->unique_constraints.CreateConstraint(
+          item.first, item.second, vertices_->access());
+      if (ret.HasError() ||
+          ret.GetValue() != UniqueConstraints::CreationStatus::SUCCESS)
+        throw RecoveryFailure("The unique constraint must be created here!");
+    }
   };
 
   // Array of all discovered snapshots, ordered by name.
   std::vector<std::pair<std::filesystem::path, std::string>> snapshot_files;
   std::error_code error_code;
-  for (const auto &item :
-       std::filesystem::directory_iterator(snapshot_directory_, error_code)) {
-    if (!item.is_regular_file()) continue;
-    try {
-      auto info = ReadSnapshotInfo(item.path());
-      snapshot_files.emplace_back(item.path(), info.uuid);
-    } catch (const RecoveryFailure &) {
-      continue;
+  if (utils::DirExists(snapshot_directory_)) {
+    for (const auto &item :
+         std::filesystem::directory_iterator(snapshot_directory_, error_code)) {
+      if (!item.is_regular_file()) continue;
+      try {
+        auto info = ReadSnapshotInfo(item.path());
+        snapshot_files.emplace_back(item.path(), info.uuid);
+      } catch (const RecoveryFailure &) {
+        continue;
+      }
     }
+    CHECK(!error_code) << "Couldn't recover data because an error occurred: "
+                       << error_code.message() << "!";
   }
-  CHECK(!error_code) << "Couldn't recover data because an error occurred: "
-                     << error_code.message() << "!";
 
   RecoveryInfo recovery_info;
   RecoveredIndicesAndConstraints indices_constraints;
@@ -2066,7 +2163,8 @@ Durability::RecoveredSnapshot Durability::LoadSnapshot(
   auto version = snapshot.Initialize(path, kSnapshotMagic);
   if (!version)
     throw RecoveryFailure("Couldn't read snapshot magic and/or version!");
-  if (*version != kVersion) throw RecoveryFailure("Invalid snapshot version!");
+  if (!IsVersionSupported(*version))
+    throw RecoveryFailure("Invalid snapshot version!");
 
   // Cleanup of loaded data in case of failure.
   bool success = false;
@@ -2448,6 +2546,29 @@ Durability::RecoveredSnapshot Durability::LoadSnapshot(
             "The existence constraint already exists!");
       }
     }
+
+    // Recover unique constraints.
+    // Snapshot version should be checked since unique constraints were
+    // implemented in later versions of snapshot.
+    if (*version >= kUniqueConstraintVersion) {
+      auto size = snapshot.ReadUint();
+      if (!size) throw RecoveryFailure("Invalid snapshot data!");
+      for (uint64_t i = 0; i < *size; ++i) {
+        auto label = snapshot.ReadUint();
+        if (!label) throw RecoveryFailure("Invalid snapshot data!");
+        auto properties_count = snapshot.ReadUint();
+        if (!properties_count) throw RecoveryFailure("Invalid snapshot data!");
+        std::set<PropertyId> properties;
+        for (uint64_t j = 0; j < *properties_count; ++j) {
+          auto property = snapshot.ReadUint();
+          if (!property) throw RecoveryFailure("Invalid snapshot data!");
+          properties.insert(get_property_from_id(*property));
+        }
+        AddRecoveredIndexConstraint(&indices_constraints.constraints.unique,
+                                    {get_label_from_id(*label), properties},
+                                    "The unique constraint already exists!");
+      }
+    }
   }
 
   // Recover timestamp.
@@ -2469,7 +2590,8 @@ Durability::RecoveryInfo Durability::LoadWal(
   auto version = wal.Initialize(path, kWalMagic);
   if (!version)
     throw RecoveryFailure("Couldn't read WAL magic and/or version!");
-  if (*version != kVersion) throw RecoveryFailure("Invalid WAL version!");
+  if (!IsVersionSupported(*version))
+    throw RecoveryFailure("Invalid WAL version!");
 
   // Read wal info.
   auto info = ReadWalInfo(path);
@@ -2744,6 +2866,32 @@ Durability::RecoveryInfo Durability::LoadWal(
               "The existence constraint doesn't exist!");
           break;
         }
+        case WalDeltaData::Type::UNIQUE_CONSTRAINT_CREATE: {
+          auto label_id = LabelId::FromUint(name_id_mapper_->NameToId(
+              delta.operation_label_properties.label));
+          std::set<PropertyId> property_ids;
+          for (const auto &prop : delta.operation_label_properties.properties) {
+            property_ids.insert(
+                PropertyId::FromUint(name_id_mapper_->NameToId(prop)));
+          }
+          AddRecoveredIndexConstraint(&indices_constraints->constraints.unique,
+                                      {label_id, property_ids},
+                                      "The unique constraint already exists!");
+          break;
+        }
+        case WalDeltaData::Type::UNIQUE_CONSTRAINT_DROP: {
+          auto label_id = LabelId::FromUint(name_id_mapper_->NameToId(
+              delta.operation_label_properties.label));
+          std::set<PropertyId> property_ids;
+          for (const auto &prop : delta.operation_label_properties.properties) {
+            property_ids.insert(
+                PropertyId::FromUint(name_id_mapper_->NameToId(prop)));
+          }
+          RemoveRecoveredIndexConstraint(
+              &indices_constraints->constraints.unique,
+              {label_id, property_ids}, "The unique constraint doesn't exist!");
+          break;
+        }
       }
       ret.next_timestamp = std::max(ret.next_timestamp, timestamp + 1);
       ++deltas_applied;
diff --git a/src/storage/v2/durability.hpp b/src/storage/v2/durability.hpp
index ddc780814..ac6720a9d 100644
--- a/src/storage/v2/durability.hpp
+++ b/src/storage/v2/durability.hpp
@@ -73,6 +73,8 @@ enum class Marker : uint8_t {
   DELTA_LABEL_PROPERTY_INDEX_DROP = 0x5c,
   DELTA_EXISTENCE_CONSTRAINT_CREATE = 0x5d,
   DELTA_EXISTENCE_CONSTRAINT_DROP = 0x5e,
+  DELTA_UNIQUE_CONSTRAINT_CREATE = 0x5f,
+  DELTA_UNIQUE_CONSTRAINT_DROP = 0x60,
 
   VALUE_FALSE = 0x00,
   VALUE_TRUE = 0xff,
@@ -112,6 +114,8 @@ static const Marker kMarkersAll[] = {
     Marker::DELTA_LABEL_PROPERTY_INDEX_DROP,
     Marker::DELTA_EXISTENCE_CONSTRAINT_CREATE,
     Marker::DELTA_EXISTENCE_CONSTRAINT_DROP,
+    Marker::DELTA_UNIQUE_CONSTRAINT_CREATE,
+    Marker::DELTA_UNIQUE_CONSTRAINT_DROP,
     Marker::VALUE_FALSE,
     Marker::VALUE_TRUE,
 };
@@ -233,6 +237,8 @@ struct WalDeltaData {
     LABEL_PROPERTY_INDEX_DROP,
     EXISTENCE_CONSTRAINT_CREATE,
     EXISTENCE_CONSTRAINT_DROP,
+    UNIQUE_CONSTRAINT_CREATE,
+    UNIQUE_CONSTRAINT_DROP,
   };
 
   Type type{Type::TRANSACTION_END};
@@ -267,6 +273,11 @@ struct WalDeltaData {
     std::string label;
     std::string property;
   } operation_label_property;
+
+  struct {
+    std::string label;
+    std::set<std::string> properties;
+  } operation_label_properties;
 };
 
 bool operator==(const WalDeltaData &a, const WalDeltaData &b);
@@ -277,15 +288,15 @@ bool operator!=(const WalDeltaData &a, const WalDeltaData &b);
 /// @throw RecoveryFailure
 uint64_t ReadWalDeltaHeader(Decoder *wal);
 
-/// Function used to either read the current WAL delta data. The function
-/// returns the read delta data. The WAL delta header must be read before
-/// calling this function.
+/// Function used to read the current WAL delta data. The function returns the
+/// read delta data. The WAL delta header must be read before calling this
+/// function.
 /// @throw RecoveryFailure
 WalDeltaData ReadWalDeltaData(Decoder *wal);
 
-/// Function used to either skip the current WAL delta data. The function
-/// returns the skipped delta type. The WAL delta header must be read before
-/// calling this function.
+/// Function used to skip the current WAL delta data. The function returns the
+/// skipped delta type. The WAL delta header must be read before calling this
+/// function.
 /// @throw RecoveryFailure
 WalDeltaData::Type SkipWalDeltaData(Decoder *wal);
 
@@ -297,6 +308,8 @@ enum class StorageGlobalOperation {
   LABEL_PROPERTY_INDEX_DROP,
   EXISTENCE_CONSTRAINT_CREATE,
   EXISTENCE_CONSTRAINT_DROP,
+  UNIQUE_CONSTRAINT_CREATE,
+  UNIQUE_CONSTRAINT_DROP,
 };
 
 /// Structure used to track indices and constraints during recovery.
@@ -308,6 +321,7 @@ struct RecoveredIndicesAndConstraints {
 
   struct {
     std::vector<std::pair<LabelId, PropertyId>> existence;
+    std::vector<std::pair<LabelId, std::set<PropertyId>>> unique;
   } constraints;
 };
 
@@ -331,7 +345,8 @@ class WalFile {
   void AppendTransactionEnd(uint64_t timestamp);
 
   void AppendOperation(StorageGlobalOperation operation, LabelId label,
-                       std::optional<PropertyId> property, uint64_t timestamp);
+                       const std::set<PropertyId> &properties,
+                       uint64_t timestamp);
 
   void Sync();
 
@@ -380,7 +395,7 @@ class Durability final {
                    uint64_t final_commit_timestamp);
 
   void AppendToWal(StorageGlobalOperation operation, LabelId label,
-                   std::optional<PropertyId> property,
+                   const std::set<PropertyId> &properties,
                    uint64_t final_commit_timestamp);
 
  private:
diff --git a/src/storage/v2/storage.cpp b/src/storage/v2/storage.cpp
index c3697571b..3ba75cd38 100644
--- a/src/storage/v2/storage.cpp
+++ b/src/storage/v2/storage.cpp
@@ -990,8 +990,8 @@ bool Storage::CreateIndex(LabelId label) {
   // next regular transaction after this operation. This prevents collisions of
   // commit timestamps between non-transactional operations and transactional
   // operations.
-  durability_.AppendToWal(StorageGlobalOperation::LABEL_INDEX_CREATE, label,
-                          std::nullopt, timestamp_);
+  durability_.AppendToWal(StorageGlobalOperation::LABEL_INDEX_CREATE, label, {},
+                          timestamp_);
   return true;
 }
 
@@ -1003,7 +1003,7 @@ bool Storage::CreateIndex(LabelId label, PropertyId property) {
   // For a description why using `timestamp_` is correct, see
   // `CreateIndex(LabelId label)`.
   durability_.AppendToWal(StorageGlobalOperation::LABEL_PROPERTY_INDEX_CREATE,
-                          label, property, timestamp_);
+                          label, {property}, timestamp_);
   return true;
 }
 
@@ -1012,8 +1012,8 @@ bool Storage::DropIndex(LabelId label) {
   if (!indices_.label_index.DropIndex(label)) return false;
   // For a description why using `timestamp_` is correct, see
   // `CreateIndex(LabelId label)`.
-  durability_.AppendToWal(StorageGlobalOperation::LABEL_INDEX_DROP, label,
-                          std::nullopt, timestamp_);
+  durability_.AppendToWal(StorageGlobalOperation::LABEL_INDEX_DROP, label, {},
+                          timestamp_);
   return true;
 }
 
@@ -1023,7 +1023,7 @@ bool Storage::DropIndex(LabelId label, PropertyId property) {
   // For a description why using `timestamp_` is correct, see
   // `CreateIndex(LabelId label)`.
   durability_.AppendToWal(StorageGlobalOperation::LABEL_PROPERTY_INDEX_DROP,
-                          label, property, timestamp_);
+                          label, {property}, timestamp_);
   return true;
 }
 
@@ -1042,7 +1042,7 @@ Storage::CreateExistenceConstraint(LabelId label, PropertyId property) {
   // For a description why using `timestamp_` is correct, see
   // `CreateIndex(LabelId label)`.
   durability_.AppendToWal(StorageGlobalOperation::EXISTENCE_CONSTRAINT_CREATE,
-                          label, property, timestamp_);
+                          label, {property}, timestamp_);
   return true;
 }
 
@@ -1053,7 +1053,7 @@ bool Storage::DropExistenceConstraint(LabelId label, PropertyId property) {
   // For a description why using `timestamp_` is correct, see
   // `CreateIndex(LabelId label)`.
   durability_.AppendToWal(StorageGlobalOperation::EXISTENCE_CONSTRAINT_DROP,
-                          label, property, timestamp_);
+                          label, {property}, timestamp_);
   return true;
 }
 
@@ -1061,16 +1061,31 @@ utils::BasicResult<ConstraintViolation, UniqueConstraints::CreationStatus>
 Storage::CreateUniqueConstraint(LabelId label,
                                 const std::set<PropertyId> &properties) {
   std::unique_lock<utils::RWLock> storage_guard(main_lock_);
-  // TODO(tsabolcec): Append action to the WAL.
-  return constraints_.unique_constraints.CreateConstraint(label, properties,
-                                                          vertices_.access());
+  auto ret = constraints_.unique_constraints.CreateConstraint(
+      label, properties, vertices_.access());
+  if (ret.HasError() ||
+      ret.GetValue() != UniqueConstraints::CreationStatus::SUCCESS) {
+    return ret;
+  }
+  // For a description why using `timestamp_` is correct, see
+  // `CreateIndex(LabelId label)`.
+  durability_.AppendToWal(StorageGlobalOperation::UNIQUE_CONSTRAINT_CREATE,
+                          label, properties, timestamp_);
+  return UniqueConstraints::CreationStatus::SUCCESS;
 }
 
 UniqueConstraints::DeletionStatus Storage::DropUniqueConstraint(
     LabelId label, const std::set<PropertyId> &properties) {
   std::unique_lock<utils::RWLock> storage_guard(main_lock_);
-  // TODO(tsabolcec): Append action to the WAL.
-  return constraints_.unique_constraints.DropConstraint(label, properties);
+  auto ret = constraints_.unique_constraints.DropConstraint(label, properties);
+  if (ret != UniqueConstraints::DeletionStatus::SUCCESS) {
+    return ret;
+  }
+  // For a description why using `timestamp_` is correct, see
+  // `CreateIndex(LabelId label)`.
+  durability_.AppendToWal(StorageGlobalOperation::UNIQUE_CONSTRAINT_DROP, label,
+                          properties, timestamp_);
+  return UniqueConstraints::DeletionStatus::SUCCESS;
 }
 
 ConstraintsInfo Storage::ListAllConstraints() const {
diff --git a/tests/integration/apollo_runs.yaml b/tests/integration/apollo_runs.yaml
index 56911d084..3cf4d6454 100644
--- a/tests/integration/apollo_runs.yaml
+++ b/tests/integration/apollo_runs.yaml
@@ -65,6 +65,15 @@
     - ../../../build_debug/src/mg_import_csv # mg_import_csv binary
     - ../../../build_debug/tests/integration/mg_import_csv/tester # tester binary
 
+- name: integration__durability
+  cd: durability
+  commands: ./runner.py
+  infiles:
+    - runner.py # runner script
+    - tests # tests directory
+    - ../../../build_debug/memgraph # memgraph binary
+    - ../../../build_debug/tools/src/mg_dump # memgraph dump binary
+
 #- name: integration__ha_basic
 #  cd: ha/basic
 #  commands: TIMEOUT=480 ./runner.py
diff --git a/tests/integration/durability/runner.py b/tests/integration/durability/runner.py
new file mode 100755
index 000000000..2ab1762bd
--- /dev/null
+++ b/tests/integration/durability/runner.py
@@ -0,0 +1,146 @@
+#!/usr/bin/python3 -u
+import argparse
+import atexit
+import os
+import shutil
+import subprocess
+import sys
+import tempfile
+import time
+
+
+SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
+PROJECT_DIR = os.path.normpath(os.path.join(SCRIPT_DIR, "..", "..", ".."))
+TESTS_DIR = os.path.join(SCRIPT_DIR, "tests")
+
+SNAPSHOT_FILE_NAME = "snapshot.bin"
+WAL_FILE_NAME = "wal.bin"
+DUMP_FILE_NAME = "expected.cypher"
+
+
+def wait_for_server(port, delay=0.1):
+    cmd = ["nc", "-z", "-w", "1", "127.0.0.1", str(port)]
+    while subprocess.call(cmd) != 0:
+        time.sleep(0.01)
+    time.sleep(delay)
+
+
+def sorted_content(file_path):
+    with open(file_path, 'r') as fin:
+        return sorted(list(map(lambda x: x.strip(), fin.readlines())))
+
+
+def list_to_string(data):
+    ret = "[\n"
+    for row in data:
+        ret += "    " + row + "\n"
+    ret += "]"
+    return ret
+
+
+def execute_test(memgraph_binary, dump_binary, test_directory, test_type):
+    assert test_type in ["SNAPSHOT", "WAL"], \
+        "Test type should be either 'SNAPSHOT' or 'WAL'."
+    print("\033[1;36m~~ Executing test {} ({}) ~~\033[0m"
+          .format(os.path.relpath(test_directory, TESTS_DIR), test_type))
+
+    working_data_directory = tempfile.TemporaryDirectory()
+    if test_type == "SNAPSHOT":
+        snapshots_dir = os.path.join(working_data_directory.name, "snapshots")
+        os.makedirs(snapshots_dir)
+        shutil.copy(os.path.join(test_directory, SNAPSHOT_FILE_NAME),
+                    snapshots_dir)
+    else:
+        wal_dir = os.path.join(working_data_directory.name, "wal")
+        os.makedirs(wal_dir)
+        shutil.copy(os.path.join(test_directory, WAL_FILE_NAME), wal_dir)
+
+    memgraph_args = [memgraph_binary,
+                     "--storage-recover-on-startup",
+                     "--storage-properties-on-edges",
+                     "--data-directory", working_data_directory.name]
+
+    # Start the memgraph binary
+    memgraph = subprocess.Popen(memgraph_args)
+    time.sleep(0.1)
+    assert memgraph.poll() is None, "Memgraph process died prematurely!"
+    wait_for_server(7687)
+
+    # Register cleanup function
+    @atexit.register
+    def cleanup():
+        if memgraph.poll() is None:
+            memgraph.terminate()
+        assert memgraph.wait() == 0, "Memgraph process didn't exit cleanly!"
+
+    # Execute `database dump`
+    dump_output_file = tempfile.NamedTemporaryFile()
+    dump_args = [dump_binary, "--use-ssl=false"]
+    subprocess.run(dump_args, stdout=dump_output_file, check=True)
+
+    # Shutdown the memgraph binary
+    memgraph.terminate()
+    assert memgraph.wait() == 0, "Memgraph process didn't exit cleanly!"
+
+    # Compare dump files
+    expected_dump_file = os.path.join(test_directory, DUMP_FILE_NAME)
+    assert os.path.exists(expected_dump_file), \
+        "Could not find expected dump path {}".format(expected_dump_file)
+    queries_got = sorted_content(dump_output_file.name)
+    queries_expected = sorted_content(expected_dump_file)
+    assert queries_got == queries_expected, "Expected\n{}\nto be equal to\n" \
+        "{}".format(list_to_string(queries_got),
+                    list_to_string(queries_expected))
+
+    print("\033[1;32m~~ Test successful ~~\033[0m\n")
+
+
+def find_test_directories(directory):
+    """
+    Finds all test directories. Test directory is a directory two levels below
+    the given directory which contains files 'snapshot.bin', 'wal.bin' and
+    'expected.cypher'.
+    """
+    test_dirs = []
+    for entry_version in os.listdir(directory):
+        entry_version_path = os.path.join(directory, entry_version)
+        if not os.path.isdir(entry_version_path):
+            continue
+        for test_dir in os.listdir(entry_version_path):
+            test_dir_path = os.path.join(entry_version_path, test_dir)
+            if not os.path.isdir(test_dir_path):
+                continue
+            snapshot_file = os.path.join(test_dir_path, SNAPSHOT_FILE_NAME)
+            wal_file = os.path.join(test_dir_path, WAL_FILE_NAME)
+            dump_file = os.path.join(test_dir_path, DUMP_FILE_NAME)
+            if (os.path.isfile(snapshot_file) and os.path.isfile(dump_file) and
+                    os.path.isfile(wal_file)):
+                test_dirs.append(test_dir_path)
+            else:
+                raise Exception("Missing data in test directory '{}'"
+                                .format(test_dir_path))
+    return test_dirs
+
+
+if __name__ == "__main__":
+    memgraph_binary = os.path.join(PROJECT_DIR, "build", "memgraph")
+    if not os.path.exists(memgraph_binary):
+        memgraph_binary = os.path.join(PROJECT_DIR, "build_debug", "memgraph")
+    dump_binary = os.path.join(PROJECT_DIR, "build", "tools", "src", "mg_dump")
+    if not os.path.exists(dump_binary):
+        dump_binary = os.path.join(PROJECT_DIR, "build_debug", "tools", "src",
+                                   "mg_dump")
+
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--memgraph", default=memgraph_binary)
+    parser.add_argument("--dump", default=dump_binary)
+    args = parser.parse_args()
+
+    test_directories = find_test_directories(TESTS_DIR)
+    assert len(test_directories) > 0, "No tests have been found!"
+
+    for test_directory in test_directories:
+        execute_test(args.memgraph, args.dump, test_directory, "SNAPSHOT")
+        execute_test(args.memgraph, args.dump, test_directory, "WAL")
+
+    sys.exit(0)
diff --git a/tests/integration/durability/tests/v12/test_all/expected.cypher b/tests/integration/durability/tests/v12/test_all/expected.cypher
new file mode 100644
index 000000000..d788a176b
--- /dev/null
+++ b/tests/integration/durability/tests/v12/test_all/expected.cypher
@@ -0,0 +1,11 @@
+CREATE INDEX ON :label;
+CREATE INDEX ON :label2(prop2);
+CREATE INDEX ON :label2(prop);
+CREATE CONSTRAINT ON (u:label) ASSERT EXISTS (u.ext);
+CREATE INDEX ON :__mg_vertex__(__mg_id__);
+CREATE (:__mg_vertex__:label2 {__mg_id__: 0, prop2: ["kaj", 2, Null, {prop4: -1.341}], prop: "joj", ext: 2});
+CREATE (:__mg_vertex__:label:label2 {__mg_id__: 1, prop: "joj", ext: 2});
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 1 AND v.__mg_id__ = 0 CREATE (u)-[:link {prop: -1, ext: [false, {k: "l"}]}]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 1 AND v.__mg_id__ = 1 CREATE (u)-[:link {prop: -1, ext: [false, {k: "l"}]}]->(v);
+DROP INDEX ON :__mg_vertex__(__mg_id__);
+MATCH (u) REMOVE u:__mg_vertex__, u.__mg_id__;
diff --git a/tests/integration/durability/tests/v12/test_all/snapshot.bin b/tests/integration/durability/tests/v12/test_all/snapshot.bin
new file mode 100644
index 000000000..b687336fc
Binary files /dev/null and b/tests/integration/durability/tests/v12/test_all/snapshot.bin differ
diff --git a/tests/integration/durability/tests/v12/test_all/wal.bin b/tests/integration/durability/tests/v12/test_all/wal.bin
new file mode 100644
index 000000000..9d0e36900
Binary files /dev/null and b/tests/integration/durability/tests/v12/test_all/wal.bin differ
diff --git a/tests/integration/durability/tests/v12/test_constraints/expected.cypher b/tests/integration/durability/tests/v12/test_constraints/expected.cypher
new file mode 100644
index 000000000..a4443b090
--- /dev/null
+++ b/tests/integration/durability/tests/v12/test_constraints/expected.cypher
@@ -0,0 +1,10 @@
+CREATE CONSTRAINT ON (u:label) ASSERT EXISTS (u.prop);
+CREATE CONSTRAINT ON (u:label2) ASSERT EXISTS (u.prop1);
+CREATE CONSTRAINT ON (u:labellabel) ASSERT EXISTS (u.prop);
+CREATE INDEX ON :__mg_vertex__(__mg_id__);
+CREATE (:__mg_vertex__:label {__mg_id__: 0, prop: 1});
+CREATE (:__mg_vertex__:label {__mg_id__: 1, prop: false});
+CREATE (:__mg_vertex__:label2 {__mg_id__: 2, prop1: 1});
+CREATE (:__mg_vertex__:label2 {__mg_id__: 3, prop1: 2});
+DROP INDEX ON :__mg_vertex__(__mg_id__);
+MATCH (u) REMOVE u:__mg_vertex__, u.__mg_id__;
diff --git a/tests/integration/durability/tests/v12/test_constraints/snapshot.bin b/tests/integration/durability/tests/v12/test_constraints/snapshot.bin
new file mode 100644
index 000000000..f48244714
Binary files /dev/null and b/tests/integration/durability/tests/v12/test_constraints/snapshot.bin differ
diff --git a/tests/integration/durability/tests/v12/test_constraints/wal.bin b/tests/integration/durability/tests/v12/test_constraints/wal.bin
new file mode 100644
index 000000000..4ff53ec5a
Binary files /dev/null and b/tests/integration/durability/tests/v12/test_constraints/wal.bin differ
diff --git a/tests/integration/durability/tests/v12/test_edges/expected.cypher b/tests/integration/durability/tests/v12/test_edges/expected.cypher
new file mode 100644
index 000000000..20df028e4
--- /dev/null
+++ b/tests/integration/durability/tests/v12/test_edges/expected.cypher
@@ -0,0 +1,45 @@
+CREATE INDEX ON :__mg_vertex__(__mg_id__);
+CREATE (:__mg_vertex__ {__mg_id__: 0});
+CREATE (:__mg_vertex__ {__mg_id__: 1});
+CREATE (:__mg_vertex__ {__mg_id__: 2});
+CREATE (:__mg_vertex__ {__mg_id__: 3});
+CREATE (:__mg_vertex__:label {__mg_id__: 4});
+CREATE (:__mg_vertex__:label {__mg_id__: 5});
+CREATE (:__mg_vertex__:lab {__mg_id__: 6});
+CREATE (:__mg_vertex__:lab {__mg_id__: 7});
+CREATE (:__mg_vertex__:lab {__mg_id__: 8});
+CREATE (:__mg_vertex__:lab2 {__mg_id__: 9});
+CREATE (:__mg_vertex__:lab2 {__mg_id__: 10});
+CREATE (:__mg_vertex__:lab2 {__mg_id__: 11});
+CREATE (:__mg_vertex__ {__mg_id__: 12});
+CREATE (:__mg_vertex__ {__mg_id__: 13});
+CREATE (:__mg_vertex__ {__mg_id__: 14});
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 0 AND v.__mg_id__ = 1 CREATE (u)-[:link]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 2 AND v.__mg_id__ = 3 CREATE (u)-[:link2]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 4 AND v.__mg_id__ = 5 CREATE (u)-[:link]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 4 AND v.__mg_id__ = 4 CREATE (u)-[:link]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 4 AND v.__mg_id__ = 5 CREATE (u)-[:link]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 4 AND v.__mg_id__ = 4 CREATE (u)-[:link2]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 4 AND v.__mg_id__ = 5 CREATE (u)-[:link2]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 5 AND v.__mg_id__ = 4 CREATE (u)-[:link]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 5 AND v.__mg_id__ = 5 CREATE (u)-[:link]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 5 AND v.__mg_id__ = 4 CREATE (u)-[:link2]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 5 AND v.__mg_id__ = 5 CREATE (u)-[:link2]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 8 AND v.__mg_id__ = 13 CREATE (u)-[:link88]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 8 AND v.__mg_id__ = 11 CREATE (u)-[:link88]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 8 AND v.__mg_id__ = 12 CREATE (u)-[:link88]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 9 AND v.__mg_id__ = 11 CREATE (u)-[:link88]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 9 AND v.__mg_id__ = 12 CREATE (u)-[:link88]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 9 AND v.__mg_id__ = 13 CREATE (u)-[:link88]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 10 AND v.__mg_id__ = 9 CREATE (u)-[:link3]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 10 AND v.__mg_id__ = 11 CREATE (u)-[:link88]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 10 AND v.__mg_id__ = 12 CREATE (u)-[:link88]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 10 AND v.__mg_id__ = 13 CREATE (u)-[:link88]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 11 AND v.__mg_id__ = 12 CREATE (u)-[:link3]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 11 AND v.__mg_id__ = 14 CREATE (u)-[:selfedge]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 11 AND v.__mg_id__ = 11 CREATE (u)-[:selfedge2]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 12 AND v.__mg_id__ = 13 CREATE (u)-[:link3]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 12 AND v.__mg_id__ = 12 CREATE (u)-[:selfedge2]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 13 AND v.__mg_id__ = 13 CREATE (u)-[:selfedge2]->(v);
+DROP INDEX ON :__mg_vertex__(__mg_id__);
+MATCH (u) REMOVE u:__mg_vertex__, u.__mg_id__;
diff --git a/tests/integration/durability/tests/v12/test_edges/snapshot.bin b/tests/integration/durability/tests/v12/test_edges/snapshot.bin
new file mode 100644
index 000000000..3b86dd90f
Binary files /dev/null and b/tests/integration/durability/tests/v12/test_edges/snapshot.bin differ
diff --git a/tests/integration/durability/tests/v12/test_edges/wal.bin b/tests/integration/durability/tests/v12/test_edges/wal.bin
new file mode 100644
index 000000000..af4a3b120
Binary files /dev/null and b/tests/integration/durability/tests/v12/test_edges/wal.bin differ
diff --git a/tests/integration/durability/tests/v12/test_edges_with_properties/expected.cypher b/tests/integration/durability/tests/v12/test_edges_with_properties/expected.cypher
new file mode 100644
index 000000000..43efdad8d
--- /dev/null
+++ b/tests/integration/durability/tests/v12/test_edges_with_properties/expected.cypher
@@ -0,0 +1,24 @@
+CREATE INDEX ON :__mg_vertex__(__mg_id__);
+CREATE (:__mg_vertex__ {__mg_id__: 0});
+CREATE (:__mg_vertex__ {__mg_id__: 1});
+CREATE (:__mg_vertex__ {__mg_id__: 2});
+CREATE (:__mg_vertex__ {__mg_id__: 3});
+CREATE (:__mg_vertex__ {__mg_id__: 4});
+CREATE (:__mg_vertex__ {__mg_id__: 5});
+CREATE (:__mg_vertex__ {__mg_id__: 6});
+CREATE (:__mg_vertex__ {__mg_id__: 7});
+CREATE (:__mg_vertex__ {__mg_id__: 8});
+CREATE (:__mg_vertex__ {__mg_id__: 9});
+CREATE (:__mg_vertex__ {__mg_id__: 10});
+CREATE (:__mg_vertex__ {__mg_id__: 11});
+CREATE (:__mg_vertex__ {__mg_id__: 12});
+CREATE (:__mg_vertex__ {__mg_id__: 13});
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 0 AND v.__mg_id__ = 1 CREATE (u)-[:edge]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 2 AND v.__mg_id__ = 3 CREATE (u)-[:edge {prop: 1}]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 4 AND v.__mg_id__ = 5 CREATE (u)-[:edge {prop: false}]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 6 AND v.__mg_id__ = 7 CREATE (u)-[:edge2]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 8 AND v.__mg_id__ = 9 CREATE (u)-[:edge2 {prop: -3.141}]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 10 AND v.__mg_id__ = 11 CREATE (u)-[:edgelink {prop: 1, prop2: {prop4: 9}}]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 12 AND v.__mg_id__ = 13 CREATE (u)-[:edgelink {prop: [1, Null, false, ""]}]->(v);
+DROP INDEX ON :__mg_vertex__(__mg_id__);
+MATCH (u) REMOVE u:__mg_vertex__, u.__mg_id__;
diff --git a/tests/integration/durability/tests/v12/test_edges_with_properties/snapshot.bin b/tests/integration/durability/tests/v12/test_edges_with_properties/snapshot.bin
new file mode 100644
index 000000000..bc434f4e6
Binary files /dev/null and b/tests/integration/durability/tests/v12/test_edges_with_properties/snapshot.bin differ
diff --git a/tests/integration/durability/tests/v12/test_edges_with_properties/wal.bin b/tests/integration/durability/tests/v12/test_edges_with_properties/wal.bin
new file mode 100644
index 000000000..35bc2bd33
Binary files /dev/null and b/tests/integration/durability/tests/v12/test_edges_with_properties/wal.bin differ
diff --git a/tests/integration/durability/tests/v12/test_indices/expected.cypher b/tests/integration/durability/tests/v12/test_indices/expected.cypher
new file mode 100644
index 000000000..76f25401d
--- /dev/null
+++ b/tests/integration/durability/tests/v12/test_indices/expected.cypher
@@ -0,0 +1,16 @@
+CREATE INDEX ON :label2;
+CREATE INDEX ON :label1;
+CREATE INDEX ON :label3;
+CREATE INDEX ON :label(prop);
+CREATE INDEX ON :label2(prop);
+CREATE INDEX ON :__mg_vertex__(__mg_id__);
+CREATE (:__mg_vertex__:label {__mg_id__: 0});
+CREATE (:__mg_vertex__:label {__mg_id__: 1});
+CREATE (:__mg_vertex__:label {__mg_id__: 2});
+CREATE (:__mg_vertex__:label {__mg_id__: 3, prop: 1});
+CREATE (:__mg_vertex__:label {__mg_id__: 4, prop: 2});
+CREATE (:__mg_vertex__:label {__mg_id__: 5, prop: 3});
+CREATE (:__mg_vertex__:label2 {__mg_id__: 6, prop2: 1});
+CREATE (:__mg_vertex__:label3 {__mg_id__: 7});
+DROP INDEX ON :__mg_vertex__(__mg_id__);
+MATCH (u) REMOVE u:__mg_vertex__, u.__mg_id__;
diff --git a/tests/integration/durability/tests/v12/test_indices/snapshot.bin b/tests/integration/durability/tests/v12/test_indices/snapshot.bin
new file mode 100644
index 000000000..cf751fc37
Binary files /dev/null and b/tests/integration/durability/tests/v12/test_indices/snapshot.bin differ
diff --git a/tests/integration/durability/tests/v12/test_indices/wal.bin b/tests/integration/durability/tests/v12/test_indices/wal.bin
new file mode 100644
index 000000000..ba46ff02d
Binary files /dev/null and b/tests/integration/durability/tests/v12/test_indices/wal.bin differ
diff --git a/tests/integration/durability/tests/v12/test_vertices/expected.cypher b/tests/integration/durability/tests/v12/test_vertices/expected.cypher
new file mode 100644
index 000000000..eeab7fb22
--- /dev/null
+++ b/tests/integration/durability/tests/v12/test_vertices/expected.cypher
@@ -0,0 +1,16 @@
+CREATE INDEX ON :__mg_vertex__(__mg_id__);
+CREATE (:__mg_vertex__ {__mg_id__: 0});
+CREATE (:__mg_vertex__:label {__mg_id__: 1});
+CREATE (:__mg_vertex__:label {__mg_id__: 2, prop: false});
+CREATE (:__mg_vertex__:label {__mg_id__: 3, prop: true});
+CREATE (:__mg_vertex__:label2 {__mg_id__: 4, prop: 1});
+CREATE (:__mg_vertex__:label {__mg_id__: 5, prop2: 3.141});
+CREATE (:__mg_vertex__:label6 {__mg_id__: 6, prop2: -314000000, prop3: true});
+CREATE (:__mg_vertex__:label1:label2:label3 {__mg_id__: 7});
+CREATE (:__mg_vertex__:label {__mg_id__: 8, prop: 1, prop2: 2, prop3: "str"});
+CREATE (:__mg_vertex__:label1:label2 {__mg_id__: 9, prop: {prop_nes: "kaj je"}});
+CREATE (:__mg_vertex__:label {__mg_id__: 10, prop_array: [1, false, Null, "str", {prop2: 2}]});
+CREATE (:__mg_vertex__:label:label3 {__mg_id__: 11, prop: {prop: [1, false], prop2: {}, prop3: "test2", prop4: "test"}});
+CREATE (:__mg_vertex__ {__mg_id__: 12, prop: " \n\"\'\t\\%"});
+DROP INDEX ON :__mg_vertex__(__mg_id__);
+MATCH (u) REMOVE u:__mg_vertex__, u.__mg_id__;
diff --git a/tests/integration/durability/tests/v12/test_vertices/snapshot.bin b/tests/integration/durability/tests/v12/test_vertices/snapshot.bin
new file mode 100644
index 000000000..02c7b5784
Binary files /dev/null and b/tests/integration/durability/tests/v12/test_vertices/snapshot.bin differ
diff --git a/tests/integration/durability/tests/v12/test_vertices/wal.bin b/tests/integration/durability/tests/v12/test_vertices/wal.bin
new file mode 100644
index 000000000..f5f44664a
Binary files /dev/null and b/tests/integration/durability/tests/v12/test_vertices/wal.bin differ
diff --git a/tests/integration/durability/tests/v13/test_all/expected.cypher b/tests/integration/durability/tests/v13/test_all/expected.cypher
new file mode 100644
index 000000000..3faa3dad8
--- /dev/null
+++ b/tests/integration/durability/tests/v13/test_all/expected.cypher
@@ -0,0 +1,16 @@
+CREATE INDEX ON :label;
+CREATE INDEX ON :label2(prop2);
+CREATE INDEX ON :label2(prop);
+CREATE CONSTRAINT ON (u:label) ASSERT EXISTS (u.ext);
+CREATE CONSTRAINT ON (u:label2) ASSERT u.prop2, u.prop IS UNIQUE;
+CREATE INDEX ON :__mg_vertex__(__mg_id__);
+CREATE (:__mg_vertex__:label2 {__mg_id__: 0, prop2: ["kaj", 2, Null, {prop4: -1.341}], prop: "joj", ext: 2});
+CREATE (:__mg_vertex__:label:label2 {__mg_id__: 1, prop: "joj", ext: 2});
+CREATE (:__mg_vertex__:label2 {__mg_id__: 2, prop2: 2, prop: 1});
+CREATE (:__mg_vertex__:label2 {__mg_id__: 3, prop2: 2, prop: 2});
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 1 AND v.__mg_id__ = 0 CREATE (u)-[:link {prop: -1, ext: [false, {k: "l"}]}]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 1 AND v.__mg_id__ = 1 CREATE (u)-[:link {prop: -1, ext: [false, {k: "l"}]}]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 1 AND v.__mg_id__ = 2 CREATE (u)-[:link {prop: -1, ext: [false, {k: "l"}]}]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 1 AND v.__mg_id__ = 3 CREATE (u)-[:link {prop: -1, ext: [false, {k: "l"}]}]->(v);
+DROP INDEX ON :__mg_vertex__(__mg_id__);
+MATCH (u) REMOVE u:__mg_vertex__, u.__mg_id__;
diff --git a/tests/integration/durability/tests/v13/test_all/snapshot.bin b/tests/integration/durability/tests/v13/test_all/snapshot.bin
new file mode 100644
index 000000000..9a0f2c840
Binary files /dev/null and b/tests/integration/durability/tests/v13/test_all/snapshot.bin differ
diff --git a/tests/integration/durability/tests/v13/test_all/wal.bin b/tests/integration/durability/tests/v13/test_all/wal.bin
new file mode 100644
index 000000000..84be2a409
Binary files /dev/null and b/tests/integration/durability/tests/v13/test_all/wal.bin differ
diff --git a/tests/integration/durability/tests/v13/test_constraints/expected.cypher b/tests/integration/durability/tests/v13/test_constraints/expected.cypher
new file mode 100644
index 000000000..87a2720b6
--- /dev/null
+++ b/tests/integration/durability/tests/v13/test_constraints/expected.cypher
@@ -0,0 +1,6 @@
+CREATE CONSTRAINT ON (u:label2) ASSERT EXISTS (u.ext2);
+CREATE CONSTRAINT ON (u:label) ASSERT EXISTS (u.ext);
+CREATE CONSTRAINT ON (u:label2) ASSERT u.a, u.b IS UNIQUE;
+CREATE CONSTRAINT ON (u:label) ASSERT u.a IS UNIQUE;
+CREATE CONSTRAINT ON (u:label) ASSERT u.b IS UNIQUE;
+CREATE CONSTRAINT ON (u:label) ASSERT u.c IS UNIQUE;
diff --git a/tests/integration/durability/tests/v13/test_constraints/snapshot.bin b/tests/integration/durability/tests/v13/test_constraints/snapshot.bin
new file mode 100644
index 000000000..939d8da53
Binary files /dev/null and b/tests/integration/durability/tests/v13/test_constraints/snapshot.bin differ
diff --git a/tests/integration/durability/tests/v13/test_constraints/wal.bin b/tests/integration/durability/tests/v13/test_constraints/wal.bin
new file mode 100644
index 000000000..0c2351e4a
Binary files /dev/null and b/tests/integration/durability/tests/v13/test_constraints/wal.bin differ
diff --git a/tests/integration/durability/tests/v13/test_edges/expected.cypher b/tests/integration/durability/tests/v13/test_edges/expected.cypher
new file mode 100644
index 000000000..79ae46b83
--- /dev/null
+++ b/tests/integration/durability/tests/v13/test_edges/expected.cypher
@@ -0,0 +1,58 @@
+CREATE INDEX ON :__mg_vertex__(__mg_id__);
+CREATE (:__mg_vertex__ {__mg_id__: 0});
+CREATE (:__mg_vertex__ {__mg_id__: 1});
+CREATE (:__mg_vertex__ {__mg_id__: 2});
+CREATE (:__mg_vertex__ {__mg_id__: 3});
+CREATE (:__mg_vertex__ {__mg_id__: 4});
+CREATE (:__mg_vertex__ {__mg_id__: 5});
+CREATE (:__mg_vertex__ {__mg_id__: 6});
+CREATE (:__mg_vertex__ {__mg_id__: 7});
+CREATE (:__mg_vertex__ {__mg_id__: 8});
+CREATE (:__mg_vertex__ {__mg_id__: 9});
+CREATE (:__mg_vertex__ {__mg_id__: 10});
+CREATE (:__mg_vertex__ {__mg_id__: 11});
+CREATE (:__mg_vertex__ {__mg_id__: 12});
+CREATE (:__mg_vertex__ {__mg_id__: 13});
+CREATE (:__mg_vertex__:label {__mg_id__: 14});
+CREATE (:__mg_vertex__:label {__mg_id__: 15});
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 0 AND v.__mg_id__ = 1 CREATE (u)-[:edge]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 2 AND v.__mg_id__ = 3 CREATE (u)-[:edge {prop: 11}]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 4 AND v.__mg_id__ = 5 CREATE (u)-[:edge {prop: true}]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 6 AND v.__mg_id__ = 7 CREATE (u)-[:edge2]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 8 AND v.__mg_id__ = 9 CREATE (u)-[:edge2 {prop: -3.141}]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 10 AND v.__mg_id__ = 11 CREATE (u)-[:edgelink {prop: {prop: 1, prop2: {prop4: 9}}}]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 12 AND v.__mg_id__ = 13 CREATE (u)-[:edgelink {prop: [1, Null, false, "\n\n\n\n\\\"\"\n\t"]}]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 0 CREATE (u)-[:testedge]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 1 CREATE (u)-[:testedge]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 2 CREATE (u)-[:testedge]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 3 CREATE (u)-[:testedge]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 4 CREATE (u)-[:testedge]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 5 CREATE (u)-[:testedge]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 6 CREATE (u)-[:testedge]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 7 CREATE (u)-[:testedge]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 8 CREATE (u)-[:testedge]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 9 CREATE (u)-[:testedge]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 10 CREATE (u)-[:testedge]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 11 CREATE (u)-[:testedge]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 12 CREATE (u)-[:testedge]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 13 CREATE (u)-[:testedge]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 14 CREATE (u)-[:testedge]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 14 AND v.__mg_id__ = 15 CREATE (u)-[:testedge]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 0 CREATE (u)-[:testedge]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 1 CREATE (u)-[:testedge]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 2 CREATE (u)-[:testedge]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 3 CREATE (u)-[:testedge]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 4 CREATE (u)-[:testedge]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 5 CREATE (u)-[:testedge]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 6 CREATE (u)-[:testedge]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 7 CREATE (u)-[:testedge]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 8 CREATE (u)-[:testedge]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 9 CREATE (u)-[:testedge]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 10 CREATE (u)-[:testedge]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 11 CREATE (u)-[:testedge]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 12 CREATE (u)-[:testedge]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 13 CREATE (u)-[:testedge]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 14 CREATE (u)-[:testedge]->(v);
+MATCH (u:__mg_vertex__), (v:__mg_vertex__) WHERE u.__mg_id__ = 15 AND v.__mg_id__ = 15 CREATE (u)-[:testedge]->(v);
+DROP INDEX ON :__mg_vertex__(__mg_id__);
+MATCH (u) REMOVE u:__mg_vertex__, u.__mg_id__;
diff --git a/tests/integration/durability/tests/v13/test_edges/snapshot.bin b/tests/integration/durability/tests/v13/test_edges/snapshot.bin
new file mode 100644
index 000000000..0c771da14
Binary files /dev/null and b/tests/integration/durability/tests/v13/test_edges/snapshot.bin differ
diff --git a/tests/integration/durability/tests/v13/test_edges/wal.bin b/tests/integration/durability/tests/v13/test_edges/wal.bin
new file mode 100644
index 000000000..69bc837b5
Binary files /dev/null and b/tests/integration/durability/tests/v13/test_edges/wal.bin differ
diff --git a/tests/integration/durability/tests/v13/test_indices/expected.cypher b/tests/integration/durability/tests/v13/test_indices/expected.cypher
new file mode 100644
index 000000000..75a978130
--- /dev/null
+++ b/tests/integration/durability/tests/v13/test_indices/expected.cypher
@@ -0,0 +1,4 @@
+CREATE INDEX ON :label2;
+CREATE INDEX ON :label(prop);
+CREATE INDEX ON :label(prop2);
+CREATE INDEX ON :label2(prop2);
diff --git a/tests/integration/durability/tests/v13/test_indices/snapshot.bin b/tests/integration/durability/tests/v13/test_indices/snapshot.bin
new file mode 100644
index 000000000..85fd00b19
Binary files /dev/null and b/tests/integration/durability/tests/v13/test_indices/snapshot.bin differ
diff --git a/tests/integration/durability/tests/v13/test_indices/wal.bin b/tests/integration/durability/tests/v13/test_indices/wal.bin
new file mode 100644
index 000000000..096167b51
Binary files /dev/null and b/tests/integration/durability/tests/v13/test_indices/wal.bin differ
diff --git a/tests/integration/durability/tests/v13/test_vertices/expected.cypher b/tests/integration/durability/tests/v13/test_vertices/expected.cypher
new file mode 100644
index 000000000..eeab7fb22
--- /dev/null
+++ b/tests/integration/durability/tests/v13/test_vertices/expected.cypher
@@ -0,0 +1,16 @@
+CREATE INDEX ON :__mg_vertex__(__mg_id__);
+CREATE (:__mg_vertex__ {__mg_id__: 0});
+CREATE (:__mg_vertex__:label {__mg_id__: 1});
+CREATE (:__mg_vertex__:label {__mg_id__: 2, prop: false});
+CREATE (:__mg_vertex__:label {__mg_id__: 3, prop: true});
+CREATE (:__mg_vertex__:label2 {__mg_id__: 4, prop: 1});
+CREATE (:__mg_vertex__:label {__mg_id__: 5, prop2: 3.141});
+CREATE (:__mg_vertex__:label6 {__mg_id__: 6, prop2: -314000000, prop3: true});
+CREATE (:__mg_vertex__:label1:label2:label3 {__mg_id__: 7});
+CREATE (:__mg_vertex__:label {__mg_id__: 8, prop: 1, prop2: 2, prop3: "str"});
+CREATE (:__mg_vertex__:label1:label2 {__mg_id__: 9, prop: {prop_nes: "kaj je"}});
+CREATE (:__mg_vertex__:label {__mg_id__: 10, prop_array: [1, false, Null, "str", {prop2: 2}]});
+CREATE (:__mg_vertex__:label:label3 {__mg_id__: 11, prop: {prop: [1, false], prop2: {}, prop3: "test2", prop4: "test"}});
+CREATE (:__mg_vertex__ {__mg_id__: 12, prop: " \n\"\'\t\\%"});
+DROP INDEX ON :__mg_vertex__(__mg_id__);
+MATCH (u) REMOVE u:__mg_vertex__, u.__mg_id__;
diff --git a/tests/integration/durability/tests/v13/test_vertices/snapshot.bin b/tests/integration/durability/tests/v13/test_vertices/snapshot.bin
new file mode 100644
index 000000000..e1b398cf2
Binary files /dev/null and b/tests/integration/durability/tests/v13/test_vertices/snapshot.bin differ
diff --git a/tests/integration/durability/tests/v13/test_vertices/wal.bin b/tests/integration/durability/tests/v13/test_vertices/wal.bin
new file mode 100644
index 000000000..7c6f1e1a5
Binary files /dev/null and b/tests/integration/durability/tests/v13/test_vertices/wal.bin differ
diff --git a/tests/unit/storage_v2_decoder_encoder.cpp b/tests/unit/storage_v2_decoder_encoder.cpp
index 63ad13394..d4a658e72 100644
--- a/tests/unit/storage_v2_decoder_encoder.cpp
+++ b/tests/unit/storage_v2_decoder_encoder.cpp
@@ -339,6 +339,8 @@ TEST_F(DecoderEncoderTest, PropertyValueInvalidMarker) {
         case storage::Marker::DELTA_LABEL_PROPERTY_INDEX_DROP:
         case storage::Marker::DELTA_EXISTENCE_CONSTRAINT_CREATE:
         case storage::Marker::DELTA_EXISTENCE_CONSTRAINT_DROP:
+        case storage::Marker::DELTA_UNIQUE_CONSTRAINT_CREATE:
+        case storage::Marker::DELTA_UNIQUE_CONSTRAINT_DROP:
         case storage::Marker::VALUE_FALSE:
         case storage::Marker::VALUE_TRUE:
           valid_marker = false;
diff --git a/tests/unit/storage_v2_durability.cpp b/tests/unit/storage_v2_durability.cpp
index cbfd29a7f..4a8817f30 100644
--- a/tests/unit/storage_v2_durability.cpp
+++ b/tests/unit/storage_v2_durability.cpp
@@ -65,6 +65,7 @@ class DurabilityTest : public ::testing::TestWithParam<bool> {
     auto label_indexed = store->NameToLabel("base_indexed");
     auto label_unindexed = store->NameToLabel("base_unindexed");
     auto property_id = store->NameToProperty("id");
+    auto property_extra = store->NameToProperty("extra");
     auto et1 = store->NameToEdgeType("base_et1");
     auto et2 = store->NameToEdgeType("base_et2");
 
@@ -78,6 +79,12 @@ class DurabilityTest : public ::testing::TestWithParam<bool> {
     ASSERT_FALSE(store->CreateExistenceConstraint(label_unindexed, property_id)
                      .HasError());
 
+    // Create unique constraint.
+    ASSERT_FALSE(store
+                     ->CreateUniqueConstraint(label_unindexed,
+                                              {property_id, property_extra})
+                     .HasError());
+
     // Create vertices.
     for (uint64_t i = 0; i < kNumBaseVertices; ++i) {
       auto acc = store->Access();
@@ -147,6 +154,10 @@ class DurabilityTest : public ::testing::TestWithParam<bool> {
     ASSERT_FALSE(store->CreateExistenceConstraint(label_unused, property_count)
                      .HasError());
 
+    // Create unique constraint.
+    ASSERT_FALSE(store->CreateUniqueConstraint(label_unused, {property_count})
+                     .HasError());
+
     // Storage accessor.
     std::optional<storage::Storage::Accessor> acc;
     if (single_transaction) acc.emplace(store->Access());
@@ -199,6 +210,7 @@ class DurabilityTest : public ::testing::TestWithParam<bool> {
     auto base_label_indexed = store->NameToLabel("base_indexed");
     auto base_label_unindexed = store->NameToLabel("base_unindexed");
     auto property_id = store->NameToProperty("id");
+    auto property_extra = store->NameToProperty("extra");
     auto et1 = store->NameToEdgeType("base_et1");
     auto et2 = store->NameToEdgeType("base_et2");
 
@@ -247,11 +259,17 @@ class DurabilityTest : public ::testing::TestWithParam<bool> {
         case DatasetType::ONLY_BASE:
           ASSERT_THAT(info.existence, UnorderedElementsAre(std::make_pair(
                                           base_label_unindexed, property_id)));
+          ASSERT_THAT(info.unique, UnorderedElementsAre(std::make_pair(
+                                       base_label_unindexed,
+                                       std::set{property_id, property_extra})));
           break;
         case DatasetType::ONLY_EXTENDED:
           ASSERT_THAT(info.existence,
                       UnorderedElementsAre(std::make_pair(extended_label_unused,
                                                           property_count)));
+          ASSERT_THAT(info.unique,
+                      UnorderedElementsAre(std::make_pair(
+                          extended_label_unused, std::set{property_count})));
           break;
         case DatasetType::ONLY_BASE_WITH_EXTENDED_INDICES_AND_CONSTRAINTS:
         case DatasetType::ONLY_EXTENDED_WITH_BASE_INDICES_AND_CONSTRAINTS:
@@ -261,6 +279,12 @@ class DurabilityTest : public ::testing::TestWithParam<bool> {
               UnorderedElementsAre(
                   std::make_pair(base_label_unindexed, property_id),
                   std::make_pair(extended_label_unused, property_count)));
+          ASSERT_THAT(info.unique,
+                      UnorderedElementsAre(
+                          std::make_pair(base_label_unindexed,
+                                         std::set{property_id, property_extra}),
+                          std::make_pair(extended_label_unused,
+                                         std::set{property_count})));
           break;
       }
     }
@@ -1406,6 +1430,7 @@ TEST_P(DurabilityTest, WalCreateInSingleTransaction) {
     ASSERT_EQ(indices.label_property.size(), 0);
     auto constraints = store.ListAllConstraints();
     ASSERT_EQ(constraints.existence.size(), 0);
+    ASSERT_EQ(constraints.unique.size(), 0);
     auto acc = store.Access();
     {
       auto v1 = acc.FindVertex(gid_v1, storage::View::OLD);
@@ -1509,17 +1534,21 @@ TEST_P(DurabilityTest, WalCreateAndRemoveEverything) {
     CreateBaseDataset(&store, GetParam());
     CreateExtendedDataset(&store);
     auto indices = store.ListAllIndices();
-    for (auto index : indices.label) {
+    for (const auto &index : indices.label) {
       ASSERT_TRUE(store.DropIndex(index));
     }
-    for (auto index : indices.label_property) {
+    for (const auto &index : indices.label_property) {
       ASSERT_TRUE(store.DropIndex(index.first, index.second));
     }
     auto constraints = store.ListAllConstraints();
-    for (auto constraint : constraints.existence) {
+    for (const auto &constraint : constraints.existence) {
       ASSERT_TRUE(
           store.DropExistenceConstraint(constraint.first, constraint.second));
     }
+    for (const auto &constraint : constraints.unique) {
+      ASSERT_EQ(store.DropUniqueConstraint(constraint.first, constraint.second),
+                storage::UniqueConstraints::DeletionStatus::SUCCESS);
+    }
     auto acc = store.Access();
     for (auto vertex : acc.Vertices(storage::View::OLD)) {
       ASSERT_TRUE(acc.DetachDeleteVertex(&vertex).HasValue());
@@ -1542,6 +1571,7 @@ TEST_P(DurabilityTest, WalCreateAndRemoveEverything) {
     ASSERT_EQ(indices.label_property.size(), 0);
     auto constraints = store.ListAllConstraints();
     ASSERT_EQ(constraints.existence.size(), 0);
+    ASSERT_EQ(constraints.unique.size(), 0);
     auto acc = store.Access();
     uint64_t count = 0;
     auto iterable = acc.Vertices(storage::View::OLD);
diff --git a/tests/unit/storage_v2_wal_file.cpp b/tests/unit/storage_v2_wal_file.cpp
index 6b13bbf1e..fd2fd5443 100644
--- a/tests/unit/storage_v2_wal_file.cpp
+++ b/tests/unit/storage_v2_wal_file.cpp
@@ -28,6 +28,10 @@ storage::WalDeltaData::Type StorageGlobalOperationToWalDeltaDataType(
       return storage::WalDeltaData::Type::EXISTENCE_CONSTRAINT_CREATE;
     case storage::StorageGlobalOperation::EXISTENCE_CONSTRAINT_DROP:
       return storage::WalDeltaData::Type::EXISTENCE_CONSTRAINT_DROP;
+    case storage::StorageGlobalOperation::UNIQUE_CONSTRAINT_CREATE:
+      return storage::WalDeltaData::Type::UNIQUE_CONSTRAINT_CREATE;
+    case storage::StorageGlobalOperation::UNIQUE_CONSTRAINT_DROP:
+      return storage::WalDeltaData::Type::UNIQUE_CONSTRAINT_DROP;
   }
 }
 
@@ -207,13 +211,14 @@ class DeltaGenerator final {
 
   void AppendOperation(storage::StorageGlobalOperation operation,
                        const std::string &label,
-                       std::optional<std::string> property = std::nullopt) {
+                       const std::set<std::string> properties = {}) {
     auto label_id = storage::LabelId::FromUint(mapper_.NameToId(label));
-    std::optional<storage::PropertyId> property_id;
-    if (property) {
-      property_id = storage::PropertyId::FromUint(mapper_.NameToId(*property));
+    std::set<storage::PropertyId> property_ids;
+    for (const auto &property : properties) {
+      property_ids.insert(
+          storage::PropertyId::FromUint(mapper_.NameToId(property)));
     }
-    wal_file_.AppendOperation(operation, label_id, property_id, timestamp_);
+    wal_file_.AppendOperation(operation, label_id, property_ids, timestamp_);
     if (valid_) {
       UpdateStats(timestamp_, 1);
       storage::WalDeltaData data;
@@ -228,7 +233,11 @@ class DeltaGenerator final {
         case storage::StorageGlobalOperation::EXISTENCE_CONSTRAINT_CREATE:
         case storage::StorageGlobalOperation::EXISTENCE_CONSTRAINT_DROP:
           data.operation_label_property.label = label;
-          data.operation_label_property.property = *property;
+          data.operation_label_property.property = *properties.begin();
+        case storage::StorageGlobalOperation::UNIQUE_CONSTRAINT_CREATE:
+        case storage::StorageGlobalOperation::UNIQUE_CONSTRAINT_DROP:
+          data.operation_label_properties.label = label;
+          data.operation_label_properties.properties = properties;
       }
       data_.emplace_back(timestamp_, data);
     }
@@ -515,10 +524,12 @@ GENERATE_SIMPLE_TEST(AllTransactionOperationsWithoutEnd, {
 GENERATE_SIMPLE_TEST(AllGlobalOperations, {
   OPERATION(LABEL_INDEX_CREATE, "hello");
   OPERATION(LABEL_INDEX_DROP, "hello");
-  OPERATION(LABEL_PROPERTY_INDEX_CREATE, "hello", "world");
-  OPERATION(LABEL_PROPERTY_INDEX_DROP, "hello", "world");
-  OPERATION(EXISTENCE_CONSTRAINT_CREATE, "hello", "world");
-  OPERATION(EXISTENCE_CONSTRAINT_DROP, "hello", "world");
+  OPERATION(LABEL_PROPERTY_INDEX_CREATE, "hello", {"world"});
+  OPERATION(LABEL_PROPERTY_INDEX_DROP, "hello", {"world"});
+  OPERATION(EXISTENCE_CONSTRAINT_CREATE, "hello", {"world"});
+  OPERATION(EXISTENCE_CONSTRAINT_DROP, "hello", {"world"});
+  OPERATION(UNIQUE_CONSTRAINT_CREATE, "hello", {"world", "and", "universe"});
+  OPERATION(UNIQUE_CONSTRAINT_DROP, "hello", {"world", "and", "universe"});
 });
 
 // NOLINTNEXTLINE(hicpp-special-member-functions)
@@ -578,7 +589,7 @@ TEST_P(WalFileTest, PartialData) {
       tx.AddLabel(vertex, "hello");
     });
     infos.emplace_back(gen.GetPosition(), gen.GetInfo());
-    OPERATION(LABEL_PROPERTY_INDEX_CREATE, "hello", "world");
+    OPERATION(LABEL_PROPERTY_INDEX_CREATE, "hello", {"world"});
     infos.emplace_back(gen.GetPosition(), gen.GetInfo());
     TRANSACTION(true, {
       auto vertex1 = tx.CreateVertex();