diff --git a/catalogue/OracleCatalogue.cpp b/catalogue/OracleCatalogue.cpp
index 946c577a17d9ce84f86b0dfb411e239651a6bf42..f064efd69fdeca65992aac0ff6e066b497b753ba 100644
--- a/catalogue/OracleCatalogue.cpp
+++ b/catalogue/OracleCatalogue.cpp
@@ -16,6 +16,7 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
+#include "catalogue/ArchiveFileRow.hpp"
 #include "catalogue/OracleCatalogue.hpp"
 #include "common/exception/UserError.hpp"
 #include "common/exception/Exception.hpp"
@@ -276,5 +277,108 @@ common::dataStructures::Tape OracleCatalogue::selectTapeForUpdate(rdbms::PooledC
   }
 }
 
+//------------------------------------------------------------------------------
+// filesWrittenToTape
+//------------------------------------------------------------------------------
+void OracleCatalogue::filesWrittenToTape(const std::list<TapeFileWritten> &events) {
+  try {
+    if(events.empty()) {
+      return;
+    }
+
+    const auto &firstEvent = events.front();
+    checkTapeFileWrittenFieldsAreSet(firstEvent);
+    const time_t now = time(nullptr);
+    std::lock_guard<std::mutex> m_lock(m_mutex);
+    auto conn = m_connPool.getConn();
+    rdbms::AutoRollback autoRollback(conn);
+
+    const auto tape = selectTapeForUpdate(conn, firstEvent.vid);
+    uint64_t expectedFSeq = tape.lastFSeq + 1;
+    uint64_t totalCompressedBytesWritten = 0;
+
+    for(const auto &event: events) {
+      checkTapeFileWrittenFieldsAreSet(firstEvent);
+
+      if(event.vid != firstEvent.vid) {
+        throw exception::Exception(std::string("VID mismatch: expected=") + firstEvent.vid + " actual=event.vid");
+      }
+
+      if(expectedFSeq != event.fSeq) {
+        exception::Exception ex;
+        ex.getMessage() << "FSeq mismatch for tape " << firstEvent.vid << ": expected=" << expectedFSeq << " actual=" <<
+          firstEvent.fSeq;
+        throw ex;
+      }
+      expectedFSeq++;
+        
+      totalCompressedBytesWritten += event.compressedSize;
+    }
+
+    const TapeFileWritten &lastEvent = events.back();
+    updateTape(conn, rdbms::Stmt::AutocommitMode::OFF, lastEvent.vid, lastEvent.fSeq, totalCompressedBytesWritten,
+      lastEvent.tapeDrive);
+
+    const char *const sql =
+      "INSERT INTO TAPE_FILE("
+        "VID,"
+        "FSEQ,"
+        "BLOCK_ID,"
+        "COMPRESSED_SIZE_IN_BYTES,"
+        "COPY_NB,"
+        "CREATION_TIME,"
+        "ARCHIVE_FILE_ID)"
+      "VALUES("
+        ":VID,"
+        ":FSEQ,"
+        ":BLOCK_ID,"
+        ":COMPRESSED_SIZE_IN_BYTES,"
+        ":COPY_NB,"
+        ":CREATION_TIME,"
+        ":ARCHIVE_FILE_ID)";
+
+    for(const auto &event: events) {
+      checkTapeFileWrittenFieldsAreSet(firstEvent);
+
+      std::unique_ptr<common::dataStructures::ArchiveFile> archiveFile = getArchiveFile(conn, event.archiveFileId);
+
+      // If the archive file does not already exist
+      if(nullptr == archiveFile.get()) {
+        // Create one
+        ArchiveFileRow row;
+        row.archiveFileId = event.archiveFileId;
+        row.diskFileId = event.diskFileId;
+        row.diskInstance = event.diskInstance;
+        row.size = event.size;
+        row.checksumType = event.checksumType;
+        row.checksumValue = event.checksumValue;
+        row.storageClassName = event.storageClassName;
+        row.diskFilePath = event.diskFilePath;
+        row.diskFileUser = event.diskFileUser;
+        row.diskFileGroup = event.diskFileGroup;
+        row.diskFileRecoveryBlob = event.diskFileRecoveryBlob;
+        insertArchiveFile(conn, rdbms::Stmt::AutocommitMode::OFF, row);
+      } else {
+        throwIfCommonEventDataMismatch(*archiveFile, event);
+      }
+
+      auto stmt = conn.createStmt(sql, rdbms::Stmt::AutocommitMode::OFF);
+      stmt->bindString(":VID", event.vid);
+      stmt->bindUint64(":FSEQ", event.fSeq);
+      stmt->bindUint64(":BLOCK_ID", event.blockId);
+      stmt->bindUint64(":COMPRESSED_SIZE_IN_BYTES", event.compressedSize);
+      stmt->bindUint64(":COPY_NB", event.copyNb);
+      stmt->bindUint64(":CREATION_TIME", now);
+      stmt->bindUint64(":ARCHIVE_FILE_ID", event.archiveFileId);
+      stmt->executeNonQuery();
+    } // for(const auto &event: events)
+
+    conn.commit();
+
+  } catch(exception::Exception &ex) {
+    throw exception::Exception(std::string(__FUNCTION__) +  " failed: " + ex.getMessage().str());
+  }
+}
+
 } // namespace catalogue
 } // namespace cta
diff --git a/catalogue/OracleCatalogue.hpp b/catalogue/OracleCatalogue.hpp
index 224c030c307a3a96450170843b0704553c4b40f0..f2b39843ed72fbb2a73751128cabebe91c07baa8 100644
--- a/catalogue/OracleCatalogue.hpp
+++ b/catalogue/OracleCatalogue.hpp
@@ -82,16 +82,21 @@ public:
   uint64_t getNextArchiveFileId(rdbms::PooledConn &conn) override;
 
   /**
-   * Selects the specified tape within the Tape table for update.
+   * Notifies the catalogue that the specified files have been written to tape.
    *
-   * This method must be implemented by the sub-classes of RdbmsCatalogue
-   * because some database technologies directly support SELECT FOR UPDATE
-   * whilst others do not.
+   * @param events The tape file written events.
+   */
+  void filesWrittenToTape(const std::list<TapeFileWritten> &events) override;
+
+private:
+
+  /**
+   * Selects the specified tape within the Tape table for update.
    *
    * @param conn The database connection.
    * @param vid The volume identifier of the tape.
    */
-  common::dataStructures::Tape selectTapeForUpdate(rdbms::PooledConn &conn, const std::string &vid) override;
+  common::dataStructures::Tape selectTapeForUpdate(rdbms::PooledConn &conn, const std::string &vid);
 
 }; // class OracleCatalogue
 
diff --git a/catalogue/RdbmsCatalogue.cpp b/catalogue/RdbmsCatalogue.cpp
index e917188e3a6419bdfce5ec326c229805f36e1303..aff68c9bf39ec3df53e8b1bd7cb82a824dfe2dca 100644
--- a/catalogue/RdbmsCatalogue.cpp
+++ b/catalogue/RdbmsCatalogue.cpp
@@ -27,7 +27,6 @@
 #include "rdbms/AutoRollback.hpp"
 
 #include <ctype.h>
-#include <iostream>
 #include <memory>
 #include <time.h>
 
@@ -3391,7 +3390,8 @@ void RdbmsCatalogue::modifyMountPolicyComment(const common::dataStructures::Secu
 //------------------------------------------------------------------------------
 // insertArchiveFile
 //------------------------------------------------------------------------------
-void RdbmsCatalogue::insertArchiveFile(rdbms::PooledConn &conn, const ArchiveFileRow &row) {
+void RdbmsCatalogue::insertArchiveFile(rdbms::PooledConn &conn, const rdbms::Stmt::AutocommitMode autocommitMode,
+  const ArchiveFileRow &row) {
   try {
     if(!storageClassExists(conn, row.diskInstance, row.storageClassName)) {
       throw exception::UserError(std::string("Storage class ") + row.diskInstance + ":" + row.storageClassName +
@@ -3428,7 +3428,7 @@ void RdbmsCatalogue::insertArchiveFile(rdbms::PooledConn &conn, const ArchiveFil
         ":STORAGE_CLASS_NAME,"
         ":CREATION_TIME,"
         ":RECONCILIATION_TIME)";
-    auto stmt = conn.createStmt(sql, rdbms::Stmt::AutocommitMode::ON);
+    auto stmt = conn.createStmt(sql, autocommitMode);
 
     stmt->bindUint64(":ARCHIVE_FILE_ID", row.archiveFileId);
     stmt->bindString(":DISK_INSTANCE_NAME", row.diskInstance);
@@ -4079,94 +4079,16 @@ uint64_t RdbmsCatalogue::getExpectedNbArchiveRoutes(rdbms::PooledConn &conn, con
   }
 }
 
-//------------------------------------------------------------------------------
-// filesWrittenToTape
-//------------------------------------------------------------------------------
-void RdbmsCatalogue::filesWrittenToTape(const std::list<TapeFileWritten> &events) {
-  try {
-    for(const auto &event : events) {
-      fileWrittenToTape(event);
-    }
-  } catch(exception::Exception &ex) {
-    throw exception::Exception(std::string(__FUNCTION__) +  " failed: " + ex.getMessage().str());
-  }
-}
-
-//------------------------------------------------------------------------------
-// fileWrittenToTape
-//------------------------------------------------------------------------------
-void RdbmsCatalogue::fileWrittenToTape(const TapeFileWritten &event) {
-  try {
-    if(event.diskInstance.empty()) throw exception::Exception("diskInstance is an empty string");
-    if(event.diskFileId.empty()) throw exception::Exception("diskFileId is an empty string");
-    if(event.diskFilePath.empty()) throw exception::Exception("diskFilePath is an empty string");
-    if(event.diskFileUser.empty()) throw exception::Exception("diskFileUser is an empty string");
-    if(event.diskFileGroup.empty()) throw exception::Exception("diskFileGroup is an empty string");
-    if(event.diskFileRecoveryBlob.empty()) throw exception::Exception("diskFileRecoveryBlob is an empty string");
-    if(event.checksumType.empty()) throw exception::Exception("checksumType is an empty string");
-    if(event.checksumValue.empty()) throw exception::Exception("checksumValue is an empty string");
-    if(event.storageClassName.empty()) throw exception::Exception("storageClassName is an empty string");
-    if(event.vid.empty()) throw exception::Exception("vid is an empty string");
-    if(event.tapeDrive.empty()) throw exception::Exception("tapeDrive is an empty string");
-
-    const time_t now = time(nullptr);
-    std::lock_guard<std::mutex> m_lock(m_mutex);
-
-    auto conn = m_connPool.getConn();
-    rdbms::AutoRollback autoRollback(conn);
-    const common::dataStructures::Tape tape = selectTapeForUpdate(conn, event.vid);
-
-    const uint64_t expectedFSeq = tape.lastFSeq + 1;
-    if(expectedFSeq != event.fSeq) {
-      exception::Exception ex;
-      ex.getMessage() << "FSeq mismatch for tape " << event.vid << ": expected=" << expectedFSeq << " actual=" <<
-        event.fSeq;
-      throw ex;
-    }
-    updateTape(conn, event);
-
-    std::unique_ptr<common::dataStructures::ArchiveFile> archiveFile = getArchiveFile(conn, event.archiveFileId);
-
-    // If the archive file does not already exist
-    if(nullptr == archiveFile.get()) {
-      // Create one
-      ArchiveFileRow row;
-      row.archiveFileId = event.archiveFileId;
-      row.diskFileId = event.diskFileId;
-      row.diskInstance = event.diskInstance;
-      row.size = event.size;
-      row.checksumType = event.checksumType;
-      row.checksumValue = event.checksumValue;
-      row.storageClassName = event.storageClassName;
-      row.diskFilePath = event.diskFilePath;
-      row.diskFileUser = event.diskFileUser;
-      row.diskFileGroup = event.diskFileGroup;
-      row.diskFileRecoveryBlob = event.diskFileRecoveryBlob;
-      insertArchiveFile(conn, row);
-    } else {
-      throwIfCommonEventDataMismatch(*archiveFile, event);
-    }
-
-    // Create the tape file
-    common::dataStructures::TapeFile tapeFile;
-    tapeFile.vid            = event.vid;
-    tapeFile.fSeq           = event.fSeq;
-    tapeFile.blockId        = event.blockId;
-    tapeFile.compressedSize = event.compressedSize;
-    tapeFile.copyNb         = event.copyNb;
-    tapeFile.creationTime   = now;
-    insertTapeFile(conn, tapeFile, event.archiveFileId);
-
-    conn.commit();
-  } catch(exception::Exception &ex) {
-    throw exception::Exception(std::string(__FUNCTION__) +  " failed: " + ex.getMessage().str());
-  }
-}
-
 //------------------------------------------------------------------------------
 // updateTape
 //------------------------------------------------------------------------------
-void RdbmsCatalogue::updateTape(rdbms::PooledConn &conn, const TapeFileWritten &event) {
+void RdbmsCatalogue::updateTape(
+  rdbms::PooledConn &conn,
+  const rdbms::Stmt::AutocommitMode autocommitMode,
+  const std::string &vid,
+  const uint64_t lastFSeq,
+  const uint64_t compressedBytesWritten,
+  const std::string &tapeDrive) {
   try {
     const time_t now = time(nullptr);
     const char *const sql =
@@ -4177,11 +4099,11 @@ void RdbmsCatalogue::updateTape(rdbms::PooledConn &conn, const TapeFileWritten &
         "LAST_WRITE_TIME = :LAST_WRITE_TIME "
       "WHERE "
         "VID = :VID";
-    auto stmt = conn.createStmt(sql, rdbms::Stmt::AutocommitMode::ON);
-    stmt->bindString(":VID", event.vid);
-    stmt->bindUint64(":LAST_FSEQ", event.fSeq);
-    stmt->bindUint64(":DATA_IN_BYTES", event.compressedSize);
-    stmt->bindString(":LAST_WRITE_DRIVE", event.tapeDrive);
+    auto stmt = conn.createStmt(sql, autocommitMode);
+    stmt->bindString(":VID", vid);
+    stmt->bindUint64(":LAST_FSEQ", lastFSeq);
+    stmt->bindUint64(":DATA_IN_BYTES", compressedBytesWritten);
+    stmt->bindString(":LAST_WRITE_DRIVE", tapeDrive);
     stmt->bindUint64(":LAST_WRITE_TIME", now);
     stmt->executeNonQuery();
   } catch(exception::Exception &ex) {
@@ -4488,6 +4410,7 @@ std::list<TapeForWriting> RdbmsCatalogue::getTapesForWriting(const std::string &
 //------------------------------------------------------------------------------
 void RdbmsCatalogue::insertTapeFile(
   rdbms::PooledConn &conn,
+  const rdbms::Stmt::AutocommitMode autocommitMode,
   const common::dataStructures::TapeFile &tapeFile,
   const uint64_t archiveFileId) {
   try {
@@ -4509,7 +4432,7 @@ void RdbmsCatalogue::insertTapeFile(
         ":COPY_NB,"
         ":CREATION_TIME,"
         ":ARCHIVE_FILE_ID)";
-    auto stmt = conn.createStmt(sql, rdbms::Stmt::AutocommitMode::ON);
+    auto stmt = conn.createStmt(sql, autocommitMode);
 
     stmt->bindString(":VID", tapeFile.vid);
     stmt->bindUint64(":FSEQ", tapeFile.fSeq);
@@ -4757,5 +4680,31 @@ void RdbmsCatalogue::ping() {
   auto rset = stmt->executeQuery();
 }
 
+//------------------------------------------------------------------------------
+// checkTapeWrittenFilesAreSet
+//------------------------------------------------------------------------------
+void RdbmsCatalogue::checkTapeFileWrittenFieldsAreSet(const TapeFileWritten &event) {
+  try {
+    if(event.diskInstance.empty()) throw exception::Exception("diskInstance is an empty string");
+    if(event.diskFileId.empty()) throw exception::Exception("diskFileId is an empty string");
+    if(event.diskFilePath.empty()) throw exception::Exception("diskFilePath is an empty string");
+    if(event.diskFileUser.empty()) throw exception::Exception("diskFileUser is an empty string");
+    if(event.diskFileGroup.empty()) throw exception::Exception("diskFileGroup is an empty string");
+    if(event.diskFileRecoveryBlob.empty()) throw exception::Exception("diskFileRecoveryBlob is an empty string");
+    if(0 == event.size) throw exception::Exception("size is 0");
+    if(event.checksumType.empty()) throw exception::Exception("checksumType is an empty string");
+    if(event.checksumValue.empty()) throw exception::Exception("checksumValue is an empty string");
+    if(event.storageClassName.empty()) throw exception::Exception("storageClassName is an empty string");
+    if(event.vid.empty()) throw exception::Exception("vid is an empty string");
+    if(0 == event.fSeq) throw exception::Exception("fSeq is 0");
+    if(0 == event.blockId && event.fSeq != 1) throw exception::Exception("blockId is 0 and fSeq is not 1");
+    if(0 == event.compressedSize) throw exception::Exception("compressedSize is 0");
+    if(0 == event.copyNb) throw exception::Exception("copyNb is 0");
+    if(event.tapeDrive.empty()) throw exception::Exception("tapeDrive is an empty string");
+  } catch(exception::Exception &ex) {
+    throw exception::Exception(std::string("TapeFileWrittenEvent is invalid: ") + ex.getMessage().str());
+  }
+}
+
 } // namespace catalogue
 } // namespace cta
diff --git a/catalogue/RdbmsCatalogue.hpp b/catalogue/RdbmsCatalogue.hpp
index 7b74f730b1ce0599c61e3c3c4274b7608c9c9a73..92d1c63162f731c45e52795807e30c89c8b808b8 100644
--- a/catalogue/RdbmsCatalogue.hpp
+++ b/catalogue/RdbmsCatalogue.hpp
@@ -111,13 +111,6 @@ public:
    */
   std::list<TapeForWriting> getTapesForWriting(const std::string &logicalLibraryName) const override;
 
-  /**
-   * Notifies the catalogue that the specified files have been written to tape.
-   *
-   * @param events The tape file written events.
-   */
-  void filesWrittenToTape(const std::list<TapeFileWritten> &events) override;
-
   /**
    * Notifies the CTA catalogue that the specified tape has been mounted in
    * order to archive files.
@@ -775,9 +768,11 @@ protected:
    * ArchiveFile table.
    *
    * @param conn The database connection.
+   * @param autocommitMode The autocommit mode of the SQL insert statement.
    * @param row The row to be inserted.
    */
-  void insertArchiveFile(rdbms::PooledConn &conn, const ArchiveFileRow &row);
+  void insertArchiveFile(rdbms::PooledConn &conn, const rdbms::Stmt::AutocommitMode autocommitMode,
+    const ArchiveFileRow &row);
 
   /**
    * Creates the database schema.
@@ -824,12 +819,14 @@ protected:
    * Inserts the specified tape file into the Tape table.
    *
    * @param conn The database connection.
+   * @param autocommitMode The autocommit mode of the SQL insert statement.
    * @param tapeFile The tape file.
    * @param archiveFileId The identifier of the archive file of which the tape
    * file is a copy.
    */
   void insertTapeFile(
     rdbms::PooledConn &conn,
+    const rdbms::Stmt::AutocommitMode autocommitMode,
     const common::dataStructures::TapeFile &tapeFile,
     const uint64_t archiveFileId);
 
@@ -852,13 +849,24 @@ protected:
   uint64_t getTapeLastFSeq(rdbms::PooledConn &conn, const std::string &vid) const;
 
   /**
-   * Updates the appropriate tape based on the occurrence of the specified
-   * event.
+   * Updates the specified tape with the specified information.
    *
    * @param conn The database connection.
-   * @param event
+   * @param autocommitMode The autocommit mode of the update statement.
+   * @param vid The volume identifier of the tape.
+   * @param lastFSeq The sequence number of the last tape file written to the
+   * tape.
+   * @param compressedBytesWritten The number of compressed bytes written to
+   * the tape.
+   * @param tapeDrive The name of the tape drive that last wrote to the tape.
    */
-  void updateTape(rdbms::PooledConn &conn, const TapeFileWritten &event);
+  void updateTape(
+    rdbms::PooledConn &conn,
+    const rdbms::Stmt::AutocommitMode autocommitMode,
+    const std::string &vid,
+    const uint64_t lastFSeq,
+    const uint64_t compressedBytesWritten,
+    const std::string &tapeDrive);
 
   /**
    * Returns the specified archive file or a nullptr pointer if it does not
@@ -913,18 +921,6 @@ protected:
    */
   virtual uint64_t getNextArchiveFileId(rdbms::PooledConn &conn) = 0;
 
-  /**
-   * Selects the specified tape within the Tape table for update.
-   *
-   * This method must be implemented by the sub-classes of RdbmsCatalogue
-   * because some database technologies directly support SELECT FOR UPDATE
-   * whilst others do not.
-   *
-   * @param conn The database connection.
-   * @param vid The volume identifier of the tape.
-   */
-  virtual common::dataStructures::Tape selectTapeForUpdate(rdbms::PooledConn &conn, const std::string &vid) = 0;
-
   /**
    * Nested class used to implement the getArchiveFileItor() method.
    */
@@ -1020,14 +1016,13 @@ protected:
     const std::string &diskInstanceName,
     const std::string &storageClassName) const;
 
-private:
-
   /**
-   * Notifies the catalogue that a file has been written to tape.
+   * Throws an exception if one of the fields of the specified event have not
+   * been set.
    *
-   * @param event The tape file written event.
+   * @param event The evnt to be checked.
    */
-  void fileWrittenToTape(const TapeFileWritten &event);
+  void checkTapeFileWrittenFieldsAreSet(const TapeFileWritten &event);
 
 }; // class RdbmsCatalogue
 
diff --git a/catalogue/SqliteCatalogue.cpp b/catalogue/SqliteCatalogue.cpp
index fd1cac0093087029c51cbb39e318e45d88a5230e..6f6baa4d14dea4fc7d8de10dd0932dfc04361451 100644
--- a/catalogue/SqliteCatalogue.cpp
+++ b/catalogue/SqliteCatalogue.cpp
@@ -16,6 +16,7 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
+#include "catalogue/ArchiveFileRow.hpp"
 #include "catalogue/SqliteCatalogueSchema.hpp"
 #include "catalogue/SqliteCatalogue.hpp"
 #include "common/exception/Exception.hpp"
@@ -128,7 +129,7 @@ uint64_t SqliteCatalogue::getNextArchiveFileId(rdbms::PooledConn &conn) {
 //------------------------------------------------------------------------------
 // selectTapeForUpdate
 //------------------------------------------------------------------------------
-common::dataStructures::Tape SqliteCatalogue::selectTapeForUpdate(rdbms::PooledConn &conn, const std::string &vid) {
+common::dataStructures::Tape SqliteCatalogue::selectTape(rdbms::PooledConn &conn, const std::string &vid) {
   try {
     const char *const sql =
       "SELECT "
@@ -218,5 +219,100 @@ common::dataStructures::Tape SqliteCatalogue::selectTapeForUpdate(rdbms::PooledC
   }
 }
 
+//------------------------------------------------------------------------------
+// filesWrittenToTape
+//------------------------------------------------------------------------------
+void SqliteCatalogue::filesWrittenToTape(const std::list<TapeFileWritten> &events) {
+  try {
+    if(events.empty()) {
+      return;
+    }
+
+    const auto &firstEvent = events.front();
+    checkTapeFileWrittenFieldsAreSet(firstEvent);
+
+    std::lock_guard<std::mutex> m_lock(m_mutex);
+    auto conn = m_connPool.getConn();
+    rdbms::AutoRollback autoRollback(conn);
+
+    const auto tape = selectTape(conn, firstEvent.vid);
+    uint64_t expectedFSeq = tape.lastFSeq + 1;
+    uint64_t totalCompressedBytesWritten = 0;
+
+    for(const auto &event: events) {
+      checkTapeFileWrittenFieldsAreSet(firstEvent);
+
+      if(event.vid != firstEvent.vid) {
+        throw exception::Exception(std::string("VID mismatch: expected=") + firstEvent.vid + " actual=event.vid");
+      }
+
+      if(expectedFSeq != event.fSeq) {
+        exception::Exception ex;
+        ex.getMessage() << "FSeq mismatch for tape " << firstEvent.vid << ": expected=" << expectedFSeq << " actual=" <<
+          firstEvent.fSeq;
+        throw ex;
+      }
+      expectedFSeq++;
+        
+      totalCompressedBytesWritten += event.compressedSize;
+    }
+
+    const TapeFileWritten &lastEvent = events.back();
+    updateTape(conn, rdbms::Stmt::AutocommitMode::OFF, lastEvent.vid, lastEvent.fSeq, totalCompressedBytesWritten,
+      lastEvent.tapeDrive);
+
+    for(const auto &event : events) {
+      fileWrittenToTape(conn, event);
+    }
+    conn.commit();
+  } catch(exception::Exception &ex) {
+    throw exception::Exception(std::string(__FUNCTION__) +  " failed: " + ex.getMessage().str());
+  }
+}
+
+//------------------------------------------------------------------------------
+// fileWrittenToTape
+//------------------------------------------------------------------------------
+void SqliteCatalogue::fileWrittenToTape(rdbms::PooledConn &conn, const TapeFileWritten &event) {
+  try {
+    checkTapeFileWrittenFieldsAreSet(event);
+
+    const time_t now = time(nullptr);
+    std::unique_ptr<common::dataStructures::ArchiveFile> archiveFile = getArchiveFile(conn, event.archiveFileId);
+
+    // If the archive file does not already exist
+    if(nullptr == archiveFile.get()) {
+      // Create one
+      ArchiveFileRow row;
+      row.archiveFileId = event.archiveFileId;
+      row.diskFileId = event.diskFileId;
+      row.diskInstance = event.diskInstance;
+      row.size = event.size;
+      row.checksumType = event.checksumType;
+      row.checksumValue = event.checksumValue;
+      row.storageClassName = event.storageClassName;
+      row.diskFilePath = event.diskFilePath;
+      row.diskFileUser = event.diskFileUser;
+      row.diskFileGroup = event.diskFileGroup;
+      row.diskFileRecoveryBlob = event.diskFileRecoveryBlob;
+      insertArchiveFile(conn, rdbms::Stmt::AutocommitMode::OFF, row);
+    } else {
+      throwIfCommonEventDataMismatch(*archiveFile, event);
+    }
+
+    // Insert the tape file
+    common::dataStructures::TapeFile tapeFile;
+    tapeFile.vid            = event.vid;
+    tapeFile.fSeq           = event.fSeq;
+    tapeFile.blockId        = event.blockId;
+    tapeFile.compressedSize = event.compressedSize;
+    tapeFile.copyNb         = event.copyNb;
+    tapeFile.creationTime   = now;
+    insertTapeFile(conn, rdbms::Stmt::AutocommitMode::OFF, tapeFile, event.archiveFileId);
+  } catch(exception::Exception &ex) {
+    throw exception::Exception(std::string(__FUNCTION__) +  " failed: " + ex.getMessage().str());
+  }
+}
+
 } // namespace catalogue
 } // namespace cta
diff --git a/catalogue/SqliteCatalogue.hpp b/catalogue/SqliteCatalogue.hpp
index 4c96ba45fb574b39960abcd395a4c78b775dfcc8..b91339f95eb53ba8a98d3e99e3a881ac8bed7944 100644
--- a/catalogue/SqliteCatalogue.hpp
+++ b/catalogue/SqliteCatalogue.hpp
@@ -76,19 +76,32 @@ protected:
    * @return A unique archive ID that can be used by a new archive file within
    * the catalogue.
    */
-  virtual uint64_t getNextArchiveFileId(rdbms::PooledConn &conn) override;
+  uint64_t getNextArchiveFileId(rdbms::PooledConn &conn) override;
 
   /**
-   * Selects the specified tape within th eTape table for update.
+   * Notifies the catalogue that the specified files have been written to tape.
    *
-   * This method must be implemented by the sub-classes of RdbmsCatalogue
-   * because some database technologies directly support SELECT FOR UPDATE
-   * whilst others do not.
+   * @param events The tape file written events.
+   */
+  void filesWrittenToTape(const std::list<TapeFileWritten> &events) override;
+
+private:
+
+  /**
+   * Notifies the catalogue that a file has been written to tape.
+   *
+   * @param conn The database connection.
+   * @param event The tape file written event.
+   */
+  void fileWrittenToTape(rdbms::PooledConn &conn, const TapeFileWritten &event);
+
+  /**
+   * Selects the specified tape within the Tape table.
    *
    * @param conn The database connection.
    * @param vid The volume identifier of the tape.
    */
-  virtual common::dataStructures::Tape selectTapeForUpdate(rdbms::PooledConn &conn, const std::string &vid) override;
+  common::dataStructures::Tape selectTape(rdbms::PooledConn &conn, const std::string &vid);
 
 }; // class SqliteCatalogue