From 6f83fff17173b5148e9c1e1b9d44a74f23b67962 Mon Sep 17 00:00:00 2001
From: Tonko Sabolcec <tonko.sabolcec@memgraph.io>
Date: Fri, 6 Mar 2020 13:19:08 +0100
Subject: [PATCH] Implement durability functionality for unique constraints

Summary:
This diff contains a necessary functionality to save and restore unique
constraint operations. The previous snapshot/WAL version is backward
compatible. Integration tests for migration from older snapshot and WAL
versions are also included.

Reviewers: mferencevic

Reviewed By: mferencevic

Subscribers: pullbot

Differential Revision: https://phabricator.memgraph.io/D2680
---
 src/storage/v2/durability.cpp                 | 198 +++++++++++++++---
 src/storage/v2/durability.hpp                 |  31 ++-
 src/storage/v2/storage.cpp                    |  41 ++--
 tests/integration/apollo_runs.yaml            |   9 +
 tests/integration/durability/runner.py        | 146 +++++++++++++
 .../tests/v12/test_all/expected.cypher        |  11 +
 .../tests/v12/test_all/snapshot.bin           | Bin 0 -> 946 bytes
 .../durability/tests/v12/test_all/wal.bin     | Bin 0 -> 1023 bytes
 .../v12/test_constraints/expected.cypher      |  10 +
 .../tests/v12/test_constraints/snapshot.bin   | Bin 0 -> 642 bytes
 .../tests/v12/test_constraints/wal.bin        | Bin 0 -> 1437 bytes
 .../tests/v12/test_edges/expected.cypher      |  45 ++++
 .../tests/v12/test_edges/snapshot.bin         | Bin 0 -> 3123 bytes
 .../durability/tests/v12/test_edges/wal.bin   | Bin 0 -> 5226 bytes
 .../expected.cypher                           |  24 +++
 .../test_edges_with_properties/snapshot.bin   | Bin 0 -> 1606 bytes
 .../v12/test_edges_with_properties/wal.bin    | Bin 0 -> 1111 bytes
 .../tests/v12/test_indices/expected.cypher    |  16 ++
 .../tests/v12/test_indices/snapshot.bin       | Bin 0 -> 898 bytes
 .../durability/tests/v12/test_indices/wal.bin | Bin 0 -> 2425 bytes
 .../tests/v12/test_vertices/expected.cypher   |  16 ++
 .../tests/v12/test_vertices/snapshot.bin      | Bin 0 -> 1575 bytes
 .../tests/v12/test_vertices/wal.bin           | Bin 0 -> 1778 bytes
 .../tests/v13/test_all/expected.cypher        |  16 ++
 .../tests/v13/test_all/snapshot.bin           | Bin 0 -> 1467 bytes
 .../durability/tests/v13/test_all/wal.bin     | Bin 0 -> 1731 bytes
 .../v13/test_constraints/expected.cypher      |   6 +
 .../tests/v13/test_constraints/snapshot.bin   | Bin 0 -> 488 bytes
 .../tests/v13/test_constraints/wal.bin        | Bin 0 -> 349 bytes
 .../tests/v13/test_edges/expected.cypher      |  58 +++++
 .../tests/v13/test_edges/snapshot.bin         | Bin 0 -> 4124 bytes
 .../durability/tests/v13/test_edges/wal.bin   | Bin 0 -> 3024 bytes
 .../tests/v13/test_indices/expected.cypher    |   4 +
 .../tests/v13/test_indices/snapshot.bin       | Bin 0 -> 343 bytes
 .../durability/tests/v13/test_indices/wal.bin | Bin 0 -> 357 bytes
 .../tests/v13/test_vertices/expected.cypher   |  16 ++
 .../tests/v13/test_vertices/snapshot.bin      | Bin 0 -> 1584 bytes
 .../tests/v13/test_vertices/wal.bin           | Bin 0 -> 1778 bytes
 tests/unit/storage_v2_decoder_encoder.cpp     |   2 +
 tests/unit/storage_v2_durability.cpp          |  36 +++-
 tests/unit/storage_v2_wal_file.cpp            |  33 ++-
 41 files changed, 658 insertions(+), 60 deletions(-)
 create mode 100755 tests/integration/durability/runner.py
 create mode 100644 tests/integration/durability/tests/v12/test_all/expected.cypher
 create mode 100644 tests/integration/durability/tests/v12/test_all/snapshot.bin
 create mode 100644 tests/integration/durability/tests/v12/test_all/wal.bin
 create mode 100644 tests/integration/durability/tests/v12/test_constraints/expected.cypher
 create mode 100644 tests/integration/durability/tests/v12/test_constraints/snapshot.bin
 create mode 100644 tests/integration/durability/tests/v12/test_constraints/wal.bin
 create mode 100644 tests/integration/durability/tests/v12/test_edges/expected.cypher
 create mode 100644 tests/integration/durability/tests/v12/test_edges/snapshot.bin
 create mode 100644 tests/integration/durability/tests/v12/test_edges/wal.bin
 create mode 100644 tests/integration/durability/tests/v12/test_edges_with_properties/expected.cypher
 create mode 100644 tests/integration/durability/tests/v12/test_edges_with_properties/snapshot.bin
 create mode 100644 tests/integration/durability/tests/v12/test_edges_with_properties/wal.bin
 create mode 100644 tests/integration/durability/tests/v12/test_indices/expected.cypher
 create mode 100644 tests/integration/durability/tests/v12/test_indices/snapshot.bin
 create mode 100644 tests/integration/durability/tests/v12/test_indices/wal.bin
 create mode 100644 tests/integration/durability/tests/v12/test_vertices/expected.cypher
 create mode 100644 tests/integration/durability/tests/v12/test_vertices/snapshot.bin
 create mode 100644 tests/integration/durability/tests/v12/test_vertices/wal.bin
 create mode 100644 tests/integration/durability/tests/v13/test_all/expected.cypher
 create mode 100644 tests/integration/durability/tests/v13/test_all/snapshot.bin
 create mode 100644 tests/integration/durability/tests/v13/test_all/wal.bin
 create mode 100644 tests/integration/durability/tests/v13/test_constraints/expected.cypher
 create mode 100644 tests/integration/durability/tests/v13/test_constraints/snapshot.bin
 create mode 100644 tests/integration/durability/tests/v13/test_constraints/wal.bin
 create mode 100644 tests/integration/durability/tests/v13/test_edges/expected.cypher
 create mode 100644 tests/integration/durability/tests/v13/test_edges/snapshot.bin
 create mode 100644 tests/integration/durability/tests/v13/test_edges/wal.bin
 create mode 100644 tests/integration/durability/tests/v13/test_indices/expected.cypher
 create mode 100644 tests/integration/durability/tests/v13/test_indices/snapshot.bin
 create mode 100644 tests/integration/durability/tests/v13/test_indices/wal.bin
 create mode 100644 tests/integration/durability/tests/v13/test_vertices/expected.cypher
 create mode 100644 tests/integration/durability/tests/v13/test_vertices/snapshot.bin
 create mode 100644 tests/integration/durability/tests/v13/test_vertices/wal.bin

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 0000000000000000000000000000000000000000..b687336fccdbe5ff1f79453f97ad8aa7499fa6cd
GIT binary patch
literal 946
zcmb_ZyH3ME5Io)u1uPe?O(2w(Y?BbVg@THYI6E$4TS){dC=g#3iQhotJ1A0tf_LVS
zl7fP(MmMv&vpWy>Pt#-5zFWCdBrAZI-4@9b;2mJH*HS%Eu@R+=C}o^{>63j`xeiqB
z)VT8<{k8zLBW!wt-+yo24X!zZ0QNc6b!gv$tbOHz;N;gT2fEYzaW)^gvyatlwOo9@
zg{!bfHLJp5*eq<#%hKARe$jq6sBu_wTPV19fA+x91oW^ukcO#Y6#=Mzu*WiXq<ZrQ
zD_m`<M@$!jDLjTnERE00hP*{dGnv+mfZ}zADF<0}{ppc1Q8ro2nJ7<OJ})k9UzhYr
kHcE%Xapa3Ei3UCyN4YQ4=(>n~9v3%0E;2bn%v>Mf8)AP!#sB~S

literal 0
HcmV?d00001

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 0000000000000000000000000000000000000000..9d0e369003d6d69f81e3107c67bdfaac289e49d3
GIT binary patch
literal 1023
zcmbVLJx{_=6fKHQ4Dx*VMg~o{CT#&tR~IJ}LO>vd;k9(2ZG;#So&04c{t$nM0~5C8
zemt}>#!k2Qo^#K+_qO+U&soFTKewU{k$8aMQeHu%z2L_0^%jnf0?+AcZ{dVG_MJ}T
z>d=j@wHpNju$E2HRGvUG(_x%(VJ1(_f={^h(`xPif~y0qz%gu2{8C3A11f&m*rEn6
zeX_;50p-3KV#tsornn-JWR+B+4HHTnfr&~aE~y+|wNQp7Qk`%Mb*3(GCYrmTcGdaE
z_BDCie7>{H#1?3zPpjbEfO6CdPK7K3ad<OsGUO)cz3{{S%N1cm1C6QtPCOs?&AsDl
zMnA8REgw@7rm{()EupJJ3YM8NrZy5O<sf1QPbJixqJ@W6P0KhFN~KC-nEwqp+?dAr
PgDmpt`zx0BFWJc#W+G!B

literal 0
HcmV?d00001

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 0000000000000000000000000000000000000000..f48244714588425b15cad6b2cdd2de06e66fa0dc
GIT binary patch
literal 642
zcmaJ+Jr9B~6vWTLxcJh<l_-u)fFRaY2Pc0-3++Ud=-^ND?-}iTuOt!}$mQ<d-Sv8T
z-j<R7PH;vPKt1vlya>Dr9LHXY0k0xJDe7TJTKAafJ`Nh$hmuyEo)CenI%EEs3EGFp
zkSnYjN@9(s@brWcgB@@<+xf|y#GWadc*zE4M3E*I$TZs6=J(?gLGJc(Z!@3*G4Gz_
wypHWYg@y^L`lf6}g2$z;TzV)gnOUu5p^cWx<f(MdnYwU$WmN$*jyr(DJ|BM|#sB~S

literal 0
HcmV?d00001

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 0000000000000000000000000000000000000000..4ff53ec5a195188633b3630ea278ac1b9c2c7089
GIT binary patch
literal 1437
zcmai!OH0E*6ou>ifr1W!F1t{&3p7onWb4M2P|{EeDuW@32oXyW_2)aS_m%c0?P_}S
z<=k^-?&R^|r@rX@|L)BIR%Ql^8}AlYQI+KJD2jums)j*a<WZ0$MG?3~5r=o<a2Ssb
zusxqvFWwOp>({)jy+io7Sbi=fbu6h7y|ltY$(>8COY+IP1k~D(H~U>Kzsg^0y)#*T
zFRb;=9+W2JnrkW5bUl@9L}nsmtt}UQa-s5a+cb3Un*&OLdAOF6dRq#7TaX(mkjPj|
zNwE(uxVPXNDX!g=pr*5r-6n=5u$EG-&Avi5CyE<nET;HJM=Fn9)>A2u0Ux*=Cv*%f
zg|(Cdx8+dC%^ZkiET-hZt!6`lN4t=NHypKHj#J3-tXtLY5!#sYoU3=xH%NBgh%4r%
oJaao0ar(C%_8W?rTF=M5JLjdLhzGt~kx1N#^*3tuHIg3Zf6JJj-2eap

literal 0
HcmV?d00001

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 0000000000000000000000000000000000000000..3b86dd90fe6ade189c4a8f9b9f00f589750afa79
GIT binary patch
literal 3123
zcmb8xOK(#_3<cmoc|&;&m-l8u%Q6xqk(zD+i4|M+c_KvAP5(73egdj`{oR1c6?9YU
z_}D(4aXk0)r}MLWgUi47<wI+gk5@0P^7Bq>9NcY<!!Uld#;~+e1IDmiX|WxsO}WcD
z$gFD_x}KpM8M>LFTN%2Yp|-<aU2SK(P#n@uLx(xn{R}<G(8CNp%1~5_|AS$9oLNnV
zo@D50hMr~Ud4^tO=;fbKrLfOP6Vc5`TyX86wnldYeXfXCzLqp>e4RlN!#u`()dK2K
zBwp2sSF9F;o!L_3s0z^|gG(_#0UU<8hlqku4`KA^Ap<!ZIEpfm1E~enb5b}&tHA|f
za7D51Rv?xJGQ*~Th*H8GK#+SfyB<+8?=M;ll9O^#W-1xA(D;90^#4m3A$F;Csl1(v
z!-b3!u38)x9ZpTNTo$Wy)CP&RcGh8gCmqEWr?r!)7#&$6tz5L~kk(ivB}?=uWQiV!
zzetp*M<QiX6BNA`hetFQ(T;=5>q7>PYw?cd>fW<leShX!By#o)zfwn)TQv;1RhOU*
zt!hK7GJ9PK6K=10l(Vq?;k97vkfGHqh-kx|TQyX)ilS9NTJ_aHm~aEQpHsUr(*{Gl
zsn8hpK|YR|?7l4CfL?d2B?6mh;C#K^tXE&xC1f{23D39$w}}(pemna<v88{5$DX++
zeq|SIA1)nvzF2R4hBWc(*v=RBcoXlyMTXhTqt<1fFUCjBaeD}x>B)F;y4;Lb^XX(X
gU5rmhvv<?gXm+xkPnOHg+x2QwzO>}>DvX%^U-f-Q$p8QV

literal 0
HcmV?d00001

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 0000000000000000000000000000000000000000..af4a3b1209565a42b54fb7b05a5d57da136252e2
GIT binary patch
literal 5226
zcmai%>uVE16vgZNt#w_E4@9II5eeEP(i9O<ks>12=~M~|OPg#;G5SG8{qw!8d(PR-
z&UE@|xO0E!o|&DQ+q_@<GdsWV|L;|Hry6CSs=-ozuNqB<EB&3-@w7MD9u9iLo&IWX
zv^<>jMl0j(!FW7<Qck8>mAla8=yQId3Kqq;-Qsuo<5&5&DDpG)=-#9#y4jH`v`$zt
z$5>cTnO>Q#8Xe9lf2*4vV_{fO%+Xe?FlevLK;gEs6+XejN2`%yE<-F<7_?VrpfFUV
zY=uv;Fx)HVxF4~?puI8!h1<$j7<ud2ajqCK$5^azAQPJ~+ukW>()ZE=ba4LmPg;j_
z$~SB0xF^t5%<)sjtbwj&LZ-5Y3@@#KH9)Fu)^LT^vy<F4#2jNWYk^FB8?z>LFQtI7
zc4aemiF<}Kp_t<-ix~r5%WN3Kyq8kISi7<ryG&!JxnqdA46&H8KxV@j=Dm~x#!y9W
z%-9tg1EHAX^@te*UCYEhvQOEUl$R9h)XqOb$~R|M>Fg}G3^A7>7IPNJY&t`NmlSN&
zoS}^Dn6nO@;SMS0cyD9QK-V&x&XC|G6>!!rZO(4c8G5K<E<-Hl4768fpfFUVY=v*J
zFmAVEj_*aRFlevLK;gEs6~4{F7pswCE<-F<7_?VrpfFUVY=!Z!$tHVv&d<2_{VSvF
z!a|I`TbbSDbdHyHW^|t|lr2q+;Y-BPPMgKygipH=qwkZ(D-xk0BgE(jq%j?#u^=Hv
zKO~LmxrIZt))c`Q{)jl<{}>K=F^2bu<CSiD2UIH##OTMQ@svhr$OtidnKX{roOMvO
z;$RFP5XYMo!=WR_@TbJ_aOBv*{R1}|v32kc3Nn6$Jnny#M*%^`uad_#j`AoV$oOaE
zaU17(>?&VPLB>BPk1ihNRUeS?FUT)eqbQFB3NrpBc{J<B=NqsKu~CXK{55erEc4}Y
zDr(CIWc(ZQXxUL-%Lioq8u`2$MR_eBknwNHqwmfwABTpmM~va`h+ji_T*05^Y+6os
zORk$wxiC|BPX)AB{^Wx~UlMR5&Lloi;yO&=s_kE)0j<|=5E#Qh5=Wm6IKbvpOPon;
zP~s*W;M8jo*#DXfa3<j^@JqNQ(h~m%>Smi2vVo(w5N4Hm=x^Rd<!z|@!Sz91{3=)G
zp+9CDifo8UMP_0Tx#mtokxeqG$jpQydR%%RnVC?;K};($Gogs?kXB@7LXj;uxsS|D
zDB=xBdt_!p5z}c!W?~RIJ$RSXip)$X;&DnVGBcrwS0}B=%!DF7aaxg?2}QPj0MVC%
JnTINF{R4zc{F?v(

literal 0
HcmV?d00001

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 0000000000000000000000000000000000000000..bc434f4e6aa8503d0fd93994399e1b97977fcb7b
GIT binary patch
literal 1606
zcmb7EJ5Iwu6f}RrpNC=FT?nO%7%L<#6jXFvz{WO0pb&wmIS4iPpx`E)1BqDKnMHcR
zf)v(hc4yw~dz-zycp7a))8Cn#dnD!D6iKn<!85=Mz?TP3yecAa;`NaRhLn?e)KlBd
zZscSE%P8Pv5tLT!U46d2e%Q{t6Q3;71wwVYn*(^o(2XA-#+i|hFXm+V_p<wl6s*n8
zB)*A}+OjWg03pI2#aOm3Y(kN?CGG*Rp)sJ^92rCn#IlQj(sSObstuD>Yr&d9XoQJz
zj}61uMp}>IQBPpQ(6td6I<hq@evK`*LW(j1t>;za8L`>QD!f54Z2)xS(Qu7_BqF7a
zw4POGBwG7VA`WXEDrB@mswENitO`AkZw0RzsTqK0WV1JQB2q1hsApA`sH^KAlR6By
z6~Y+puF9M0{&sZ7vGI9oR#JMzP&PE50kX|o#(ya-8a6$OyZ&r>o#lg*VV%_3<da?3
bbuui=D#?mrIw(%Fyx;4~HhRSSyycm1jPNlf

literal 0
HcmV?d00001

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 0000000000000000000000000000000000000000..35bc2bd33fac8462710579d9eacce994885d7e29
GIT binary patch
literal 1111
zcmZuwyH3ME5H!ya9uheWSAqme=K^yIahrw;iGssv9c&{+1Q87%L(P9s@F)BT=WsK7
z#-xbWb2GEE>+|*1+k7|r{Y})duhg9{PUACQ6}2hG7g?Ru#{HT5x=ymPtP(TJ((&xl
z6r=Nz^4w^sR(yH$FNRa$S@Pm_FRjXliYN_5G99Wlc3JgRh~ZQ)OPh8VsA(vYyenC}
zzAQ9hp}IgD41_5{45xxwh1TsrP}5K(c~`OwG;mu-1DB-7B%54~V5o#=IeQG3<^`k*
zayp8}wdu3X7TXAhN_bXbv_EYir=w_c(LlVM_~7>Y<NfQT|0z_LXorcmnMg2H!n533
zj10}-^T+2W1qYgnEEicTRB$iQE1+VvP5x9L;XB|i8r<k$2k>x%?Qt-Cupx$1!7N{0
fcQ826RAjlxT7w<pNv^)$*MmsMK9{?H2r>NuMu$fi

literal 0
HcmV?d00001

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 0000000000000000000000000000000000000000..cf751fc37e9261394ff60a4ff70551eb749dd553
GIT binary patch
literal 898
zcmb7BI}U<C5XHZRjS~{ujbce6$O^VvP<aRttcVgljkOo`!_M1~B}7RGFEjH#GaQrq
zWoFgSo)oa?pb#bf8qtHnlfiCKw}htkn5Mp#ZW$Fpm<BM0OuiuHZ)yHdG?liE27d-l
zhqj=h@@pAl0kj+)*GMU7^p_GgXi*v)O6HlE-@V<$P(n(>7r)0P7T5dYOyDCDgm5pk
zxUz!wg0LJ*+FU8qtSEilv02|Bl+6*U+qDW1u{j>V5#wKZ{7akdKPpSxRa+FYi1PRA
gt+!|23*%ktWswZMG!Eq{4`f)#nC5&{tUguf1tvTqYybcN

literal 0
HcmV?d00001

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 0000000000000000000000000000000000000000..ba46ff02dcc6efca20876b53eaac0685b2ac48cb
GIT binary patch
literal 2425
zcmah}O-sW-6s+HfiggL(=t0RX&@`>BS1+D~kS<bD2)0c`s91`qKi_TLd2iF~c6-{+
z&YL%reY+3$Kl9Vp|L;y*>XmrZi>u&Tuc~y`xw%dH@vs`Cac?*+<6$|S#e=Gvr2V9t
z^pcLy+*U-Zr{GY7^Vdl^?>cu9=T6Ey&%uGI7VPVPi{<CSWYv2*X=l2;C_dhb?{fK7
z{uV`WY@+Y8qKL$fhBC^<SWZ}A)w42dF_<A)tKlM~J?4)iu}9WmlChkOyR4zwU~X6g
zL$a2|TC=yZ2Bl2wYLkrRgt@Gt+F$}&j}mK1*0NY@_Ey%8=&K<gV>x|Omo-!y%nfT`
zNY=7gYxXwy=4^-b@!6(I0m)2&%vesCD+Q_z=0*xIBx?a$UzyeA2x*Ud8+;gB-O!NE
ziK!~Z37rMnGnUg3xl*9oU_wTZ2^f;KEGbaLy$yan1s@xvRSMkwOyKchEGNvB0@Vf+
zGI%#Zkt}A3fhulp@R4zK7+rIwpsjM8(`jIa8O!P3xN@M{V8YQlhkDCFkt}A((QK}~
zwY9ewdwbi<eW6Q6^m^KBBLU{tH@MBb5#33@BA&;l!?31^x5}?b%;vYNDPmUrip2CN
i;x70ViRn?qSo#%-=~2YY`4x%jQN%p@6^Xf3ar_7CTLwh{

literal 0
HcmV?d00001

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 0000000000000000000000000000000000000000..02c7b57840bea927fbd2c71eb25b77d2d0d8c6e5
GIT binary patch
literal 1575
zcmb7EJx{_=6s_Nan#i^A2A3DaIFVr6h;Bv)UECZ^%oDmGK|)#F{2$K#3^#v=F>!Ws
zHb|l8Jet-7JDuKp-?``9+t;g$sJH1%pEEg6kOd#h(~x`v`~bYKOjBBtQ_eg<%L1AZ
z%yC}J;@`(e1fWVeTC^<vi(-_uqa`u#SQlAcRAGxU4z(oHn_*%R2q;+XTz|eizv;qh
zo~G4|EvlqASXc7&digaPKNO%!xlY%>ilf3lWMvJdXMmP<5YZNkNR5=TBSTKv&0}QB
zMKT%=@+MM1ODfH3M@x286@*+!?hhXNx4ke@F3#VZj_!ncJ&WiG7F)y!bg43}*!Cqy
zZKs^g2ts)*$eWo6Ckdcs13Ks(IpMk}2tZZv$+!S<D^&)GUCj7yxoJ5SG)TpL#n)jN
z4L!ca#DQX;IB7=(DlPac*&M{$k-2cBi`9d|&AzP#Pc0j=RaL=42wbdjvU)QZm`9F{
zA3@jLg<YTbAss)YJX(yosDpEuu<+wflX$%PERRV%{(sWtN4(6ba(~aQC4)HeJ>z-J
j<GSw~QxE)Bt8TooU2iv<jUZ@-rrii+Hz}r8*-9e6#1~j1

literal 0
HcmV?d00001

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 0000000000000000000000000000000000000000..f5f44664a21ea8aedc15f8ec02e539fc6a216363
GIT binary patch
literal 1778
zcmah~O;6iE5Dm0I!-pUnCo4`we4Zi&+iKcd)k}M+6s=Z>0}@&iUqS>?5(#nRe{kk!
zaO*$mxxXPDJNss0r%`-bKfjsxW_EV{>&Mezw={dct0TWsSAKC4och(*D0E@i{?d#h
z=bCZU?KWMQ^qRd^yA{Xn#PwRS^4!w7TJy`!f3WLw^?^TwD45~kV8hb}?k*V!gA(9N
zN3DB~V1}_`0z_G1eDvYN(c@58$T>J9vc)dpS1`jh6h#nag^?GTN){3+vj|v%83ioZ
z^t7xMp-`+)z|BhGCJL3rc32E$3TC+UVnv9ui;Pq&)K&ZE?e%w~{7tfv*dB|)ykJHF
z%UAYUC{`#iJ5y6H??3-e=~>B*bY+f2miE~ac1bY9`<=IRxtc=zfy~RYL_#4S754GT
zuLLs+SaBZ^WfvK~GjbSKa3dXHrpJ?!h|i^7HcrS)WmLHgOtD~wCwrlt2wO&>c7kFh
zOU_Rn_eOwbr5(6^NXDAw?p?ppPb3!^uCd{5?mIA}fED`=h7|^=nW`I&+_SD>N;RK7
zx={2%uG3nKgTAnJkR!jE{o)J?X81H0{UU0Pfzy^<A2f@gSV@xaFWv$yQk%Ml0n63M
z`m<8%-gGI4)5M|TNiv><{FVmAnsw5T;*WZ8$UVT1N-(Q{aeU@8!0~QuA77Mj&Lrac
EA3$N0s{jB1

literal 0
HcmV?d00001

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 0000000000000000000000000000000000000000..9a0f2c8403dc292b6b2d1bd666980e605897b628
GIT binary patch
literal 1467
zcmcJOJx;?w5QWWegOFfx*w%qiTCx@^F)b8S^t5rXC2T8+LJBI*!7aD|5(lB;5EN9P
z;GJ)gY$6Kc>S^E1o7wl)SC@C=U9bMm)rCdsX{|=;deeeO0dE3kTQ${Hw#!rN7O4#j
zwEDE4vTdI3NR{6A^r0Q(`2tD}th!20S0F8GtUy|>|L@X)^BQLWjj6hW>AsNVu+Xff
z_kILm<MgJp$u!mnuOAP^^ZfnAWr;>HDO{Y0xvY2(thOx=L#V)2lY?+Alx(m6yaS^F
z>>#E%FPTgYT>vQd1whG~c&3C3T5LiZAptHSl(ciD*~f{D<7xo{%lenj719ZC&EMzt
zTs7u4g_<WKebUlTZj1#W1Q4MkBES=0UV1f5vkKl9{pxmH5*Njb-@>q!FHV1J7+gkV
yQO-}otIh7`$9~T`k_=3en^79}&rXvtHaBsYrcpmkO+Lu8EJ{XErgm&GT7WM$pIHk4

literal 0
HcmV?d00001

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 0000000000000000000000000000000000000000..84be2a4096a8e44ec6ba381c1e1a1aaf1692987a
GIT binary patch
literal 1731
zcmb_cO-sW-5Ut;M5Ohu5qX(t8K#8S|S1+D~5~8h$NK;cV4Yo)@PyRB&AL8%u;7QYT
z-<yqXrKmX#Gw;25GrOC!)5mnHH2F@{zKPU@2@ae?6Zs9#?|I$GtsfuxZq2)`xlvTD
zyOGyx^y0YccdM~7*iwbjrL$&|X(x(PDO@@0cEKZV_%IlHKj2mkZHyDxHTiiTXT_l6
zr;&hezV%eg21Rn-+$pwLGN2}hswm_F=DtY_Bov6Pt7$PABs0<02%1i3BC+D!y)L7T
zwCHydk<i<E5B>{-*Yxh&$8+*BdViHR?TUhtCdZasz#OH_L4#!>iYG22p@_iycogxU
zuLyz`8ma7*Wj@i(ykj*@YlFxI%uO90AfZ4C&S+RVSWvJh=Rpa<X2v#nt1yJ*0<L_@
z7-*qDipFrIMa_cFH3r$t*rqXdK7DV~yZb&TXW6FkUcpq6g0fA4VZOQ|1!bFpjm@E;
zER+__l9FFA^=ZDDQ0R<rR~bI^PRV*c7^trugxNn1D{OID3$z8F|Ajxah1mt#68aC*
OLSliogd+I$wCykFTAv*N

literal 0
HcmV?d00001

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 0000000000000000000000000000000000000000..939d8da53240d23bcb0f7af8c41a02c4ec8a51d1
GIT binary patch
literal 488
zcmaKny$-@K41`Phn^>$o0hBQkO{2D~Ffg&QJ0)Ai0t0WutAc3m7(jUX<U613<KcQ)
zWJB#_pXdjiAmyWh<dL9|BxQuP<gimq?(8Ft;h6wv<~XG(Jd_wn&$skvYL)_*GDkF8
z2GN~>43@<E&GAoziC#nO9bYrp`|b-KHGo%$Adt&cFvV6_nIat*+fw7%HMUXLS|2NK
UL%sFZX;sx)Z?p`7*YZHZ6BZ>IdH?_b

literal 0
HcmV?d00001

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 0000000000000000000000000000000000000000..0c2351e4ae4d2e079140d7d1930acc98873e3a99
GIT binary patch
literal 349
zcmeZuFVEp+fB+{Uc_>RL49ZXzQGv274APP<k`t446U_}Qbxq7nEOZks(@b<tP0W)`
z3{8?u%`HrXplTV^;H+2?HmG1uVp3|3kq8Tvn_5v~gshAeS(ykkLKQ+$ya<XSAx5Yo
b5f~$p1hq*ds7*#zi|QXCCJaZwos$Fre_bLL

literal 0
HcmV?d00001

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 0000000000000000000000000000000000000000..0c771da1465e1961e73ca77411be5332c26c0946
GIT binary patch
literal 4124
zcmb7{y>3%M5QWV@Hh;;+@#UYrb`oeT#g1bX(Lq5)M@fT;K?o~EByU5_dr<HOJOUL@
z0kLw=Hx4H^5^1tJGqdN+?#%4<tCt6RHyX!(&(n))m4071UZvGr)#8a3&$ReiEy`3y
zHCUACoOZHBk*-y?GM%sYTK-d}O2p#67waQdUjO>`^+#@gFVm%ZuZ}p|+G_vH=lue~
zZN<*vVbPKP6Zoh|O_I1=Pqr$K*uPjRmK#M|;jN>;H{EWlS*9!6r~2P0(^ZEQxNz1j
z_tFtBcwKidUHpRAP4}v<dGQvO+-un(ZL9xB39YzS+o4s5It~>M-FE1XLw6mz=g@tJ
z9ys*Sp+^p7hf0UK4)q*bb7<Y6jenucEu6AYPect;2UI1;O+8>zkHpS~WEw1-Dv1s6
z^HY3i1y{zcyE0Ma7&U@|gY4-o=b((7gIrnI+{$OAh&S;FLMtuq%BW^nrtq9Jhazd&
z;$WB=&Ov)Phl|z#g)7HAhLr``leBAPbh$FR)Rl=MCnmHYq(%13Lg%3EorCO2gQieE
zIhxAp31fj)#U})@fP?Ad%EXoZ5jiD$;+=zWh7a^)&y~?ZQlEJ!gH_U^DTt@b6`y!&
zTv@I*O0G;4Ing<SLI)hYUCvR~bC5j+PY3BXGv7MFiVitNcc2xM!lDQ`{BFH6QRGDD
z2-1d)sC?dJ&%RgBLG~0Z9VGQR0Xe;#qB+ruiBAJpwpOo9TsaQS#mXs;fsYO+<9+o)
zqdd76b_SuSk%7tjC5D9;15wl&eS?WZq$n|Q8Qd5qPZ<*_lLaPnCksq;6j@+W1x9ON
zq7umh6Qw{Fn9jI3z8c9wQ8|VNx5Bw_g-i%G@{`)AykkRg#O&hRiJkG`;{_R*_~t+c
zCLVoy1ru+z7YC1}7YA<~hrotBG9WB?8@xEOj>U_kl*x;Op+<?r)l-xg2gAUNgVz0@
zi(6BT#&4c=hq^JkbZGoYoV}e)XS{E19nW<4!*up>?-NspfVpMJH}Z+!rZ9ufof#Bt
zemFfioLhQroyX?;-NEqPc+em3>}>Z(!_85D@^rG@-yTlJTa&?HYcL!o+UKzjSbhB$
D@^xKp

literal 0
HcmV?d00001

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 0000000000000000000000000000000000000000..69bc837b59acb8c3892460932d67f5238582c6ff
GIT binary patch
literal 3024
zcmai$%~BIV5XU15nuth-B`ctO1k5c}?2^Rd%7qhEDT-k^aA74{7Ad8aDj&wP@8Q8W
z@j-MF=HK06$LX9Vo&NR5{JW=ea{PV#qIUl|GF$#+&i!FG+4HCU57$}#evowshll-c
zFW>KV%XekJ+t16vL78O-S>7|=x7IO_{Banx;aB#v<d;10=;-?6h^!Qlk&BLbOj+jq
zN!su$+bo5d%>tSNGE!8L+pk}4Q}eu%(G@gL1)&RR!>??!2B~KY0!;xKDXPe{pyprp
zag{u!N*1V4w&BEnmU>T6=0t$p0H=t|xHNT2^NiB44JY=q2CY3v1Dql<Q!oQ~+VRTS
zpPxT|Z`XgZtE&jQ3{8V7VjE8EXX%a;)G%p${OR)zMWCRJ6^zXk)v5@3KQ0(~YI|bN
z(^k;lOT{&yWY-%ub!Zl;H@4x#ewNg$_J#-)l(B-bnci0MEv2PMm(yk~O(1l`eJ<PW
zMxBGXvR~4WEK%98of_EaX2w_LXv~xg(4+vs*mg?c9|i#B0<8D|mZq34<ZkrsZu-=q
z4tjGWhS7*%P*P&Jmk5S=kr*a7g7M?Vy<t)!7!;J*FbNS1PlCiSZV?P`hs1Er5sY6a
z?hV36FlZ#REqmc37&MX?Mkj(nBZ*;jA{aE17`lvL&`4s5-WxQM*s6{}BZ;+i3>rx+
z{VxV@T$=mWfQzu^m5zn>ZB55Q`?juQp?%xXvCzJ~*0IpOz0tAIzHRDQXy5QxusV~Z
sTWH_5^=zSi+tIPmzO{8Mv~RmQ7TUKx9SiN-TOAASTSv#B1TW6TKZ_^e82|tP

literal 0
HcmV?d00001

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 0000000000000000000000000000000000000000..85fd00b1981e52b72069e5576f99a219dbfb3f8c
GIT binary patch
literal 343
zcmeZuFV5p-fB+{UXDAB`V=7E)1&qPR2w|xRK^Y7P1~W_%lx7fO!VrP0foq3}s0zWg
z!5B(HEHDFLszg|!A_YbH1x9cSL|_U*!f+Rfut62)BqpWi7{M)o8v+(r7EysJvotYF
rGfgrv(KWM7G1oOoH8a(<v`9A6HL)-;H#9XgPqi>Lfg27t4bA`n-d-0#

literal 0
HcmV?d00001

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 0000000000000000000000000000000000000000..096167b5111230e9585c64c4e3f7e98f245bb26f
GIT binary patch
literal 357
zcmeZuFVEp+fB+{Uc_>RL49ZXzQGv2d3{#U+QWMQ}&5V;$bxo4eOm!_%5{-3Jl2Q^=
zlhZ7c%}tDiplTV^;H*dyR;XZ3Vp3`jvIHBlgb_j}3YV5>R0$Clr~w5<`2`5=(ITh@
W6RQm7PLOFxu0gjHhq4%awgLbGY%N>>

literal 0
HcmV?d00001

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 0000000000000000000000000000000000000000..e1b398cf27da301ff3c0b1ad6f10291bf7139ddc
GIT binary patch
literal 1584
zcmb7Fy-or_7{osm)I`>WEo?T3vAP5}xNx1(LK{0v6La7-NRV(ecD{$Tk74J7m{?d@
z`2fhl%`BHa6TE8p_S^YpX7`Tk%V@ZtTRi9DB0&axY%WUT2Oz(dRK60RMY1cm3eaK=
zLkL#buf_V`+eieUN*P+T*!UO4)V8BVl0we7uys*|X|fz@k)*f8#3m3>usXc?e0_P>
zg|jS8dCL}6QXH%+@%(o6J)J!iph}rex50|7f^*2q9!j@>7CRuKEjE!FOUgusHDyj7
zktvDAXfn=9q=1oBYHP<xPE-|?BqWc<kE6R`7%7SU4?$n|!>pM_%mj;RVg<TXnO4mB
zQcfMDoGl4Ldn~BAln9dq&|()l=pAc<$3;N^s)8#c0pd}r3=(rNvv*6T<y0^rRd*GC
zPQqx?<gb{xr8p;EG*N*{i#_;2iAfPC2}inEJuTdx*gQ46l+jfcI2i(o^PJy-U_1_<
zm>+M$An1kzmoG-T-k?r;jkzd@fy`NW%hM!HegoJZlQj7uO1FD_t5fanv8*LA-uu4m
k`$5ME>Y?Yf+U>T}se6Ir_uP7~)#!TNuqBSH?C~G*3q2ZF2><{9

literal 0
HcmV?d00001

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 0000000000000000000000000000000000000000..7c6f1e1a5e6e2db57e486aca2175e7656e7392d7
GIT binary patch
literal 1778
zcmah}%T5A85X86mh%zorJV|_>NRS1>-i#N$n2^cDcrY=G?nQ|jATi$j56^yvH~+-5
z-=MwAR5!aw*wbuHbysyy&s?0nbk}pk?^Nx$mAZ6`J#XKw?t;J%g0NKyt5LI3Z!{W}
zR<#*cf?L0OTR-kJJ5gOZZthUcxMlkv?CMa>@ivHp84mUq9IYF6qOLF~0e0GI)^P+g
zjAaub$_iuNg+KS6o^^qogF_-q>=J$jGh9Pf1W{HPX^~&aLLzw<0ZTBWfO(6Kmb9WK
z6e|>PlTx^eLM5?P7DJhW87@6r5u)rkBi5?vE&KiP{#DMONH!9~T(uR<3uYKgSJo7Y
z6$;D_)zsVP`OhzXQ@Oq_jF8CE2G@^M63p;<r!5^%r_gyI^Q0`kP{>DteJs0`U`7GU
zo&%!nIKy>DF2fYAuRY9kf6x>0k<^pM37M&kEiMC7ESTZR9&0DUCQ+!JpjgS0@>9aQ
z;h|Y^2W}ptzGk_H@V<N>NiH&6WWy`mcVI>V%k~`%D-2K*)v(tKUv&{vs`>8GgQ7Q6
zoz`L;^s%FZ9QoDk7k5xF!?!u>7f~Y&+`#1dpjiaPN|JPc@#kR?+tfu2SgJ<WAC^*b
zrgJHrCJq%3qW+-9e{oP<vrfDz{-_7r+=DefGhkK$<M_m<hwEKl**(f%A4ry~KcZ)p
AZ2$lO

literal 0
HcmV?d00001

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();