diff Odbc/Plugins/OdbcIndex.cpp @ 329:b5fb8b77ce4d

initial commit of ODBC framework
author Sebastien Jodogne <s.jodogne@gmail.com>
date Tue, 10 Aug 2021 20:08:53 +0200
parents
children 16aac0287485 cd9521e04249
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Odbc/Plugins/OdbcIndex.cpp	Tue Aug 10 20:08:53 2021 +0200
@@ -0,0 +1,694 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2021 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "OdbcIndex.h"
+
+#include "../../Framework/Common/Integer64Value.h"
+#include "../../Framework/Odbc/OdbcDatabase.h"
+#include "../../Framework/Plugins/GlobalProperties.h"
+
+#include <EmbeddedResources.h>  // Autogenerated file
+
+#include <Logging.h>
+#include <OrthancException.h>
+#include <Toolbox.h>
+
+#include <boost/algorithm/string/replace.hpp>
+
+
+// Some aliases for internal properties
+static const Orthanc::GlobalProperty GlobalProperty_LastChange = Orthanc::GlobalProperty_DatabaseInternal0;
+
+
+namespace OrthancDatabases
+{
+  static int64_t GetSQLiteLastInsert(DatabaseManager& manager)
+  {
+    DatabaseManager::CachedStatement statement(
+      STATEMENT_FROM_HERE, manager, "SELECT LAST_INSERT_ROWID()");
+    
+    statement.Execute();
+    
+    return statement.ReadInteger64(0);
+  }
+  
+  
+  static int64_t GetMySQLLastInsert(DatabaseManager& manager)
+  {
+    DatabaseManager::CachedStatement statement(
+      STATEMENT_FROM_HERE, manager, "SELECT LAST_INSERT_ID()");
+    
+    statement.Execute();
+    
+    return statement.ReadInteger64(0);
+  }
+  
+  
+  static int64_t GetMSSQLLastInsert(DatabaseManager& manager)
+  {
+    DatabaseManager::CachedStatement statement(
+      STATEMENT_FROM_HERE, manager, "SELECT @@IDENTITY");
+    
+    statement.Execute();
+    
+    return statement.ReadInteger64(0);
+  }
+  
+  
+  static void AddPatientToRecyclingOrder(DatabaseManager& manager,
+                                         int64_t patient)
+  {
+    // In the other database plugins, this is done with a trigger
+
+    std::unique_ptr<DatabaseManager::CachedStatement> statement;
+
+    switch (manager.GetDialect())
+    {
+      case Dialect_SQLite:
+      case Dialect_MySQL:
+        statement.reset(
+          new DatabaseManager::CachedStatement(
+            STATEMENT_FROM_HERE, manager, "INSERT INTO PatientRecyclingOrder VALUES(NULL, ${patient})"));
+        break;
+        
+      case Dialect_PostgreSQL:
+        statement.reset(
+          new DatabaseManager::CachedStatement(
+            STATEMENT_FROM_HERE, manager, "INSERT INTO PatientRecyclingOrder VALUES(DEFAULT, ${patient})"));
+        break;
+        
+      case Dialect_MSSQL:
+        statement.reset(
+          new DatabaseManager::CachedStatement(
+            STATEMENT_FROM_HERE, manager, "INSERT INTO PatientRecyclingOrder VALUES(${patient})"));
+        break;
+        
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+
+    statement->SetParameterType("patient", ValueType_Integer64);
+
+    Dictionary args;
+    args.SetIntegerValue("patient", patient);
+    statement->Execute(args);
+  }
+
+
+  static OrthancPluginResourceType GetParentType(OrthancPluginResourceType level)
+  {
+    switch (level)
+    {
+      case OrthancPluginResourceType_Study:
+        return OrthancPluginResourceType_Patient;
+
+      case OrthancPluginResourceType_Series:
+        return OrthancPluginResourceType_Study;
+
+      case OrthancPluginResourceType_Instance:
+        return OrthancPluginResourceType_Series;
+
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  OdbcIndex::OdbcIndex(OrthancPluginContext* context,
+                       const std::string& connectionString) :
+    IndexBackend(context),
+    maxConnectionRetries_(10),
+    connectionRetryInterval_(5),
+    connectionString_(connectionString)
+  {
+  }
+
+  
+  void OdbcIndex::SetConnectionRetryInterval(unsigned int seconds)
+  {
+    if (seconds == 0)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      connectionRetryInterval_ = seconds;
+    }
+  }
+
+
+  IDatabaseFactory* OdbcIndex::CreateDatabaseFactory()
+  {
+    return OdbcDatabase::CreateDatabaseFactory(maxConnectionRetries_, connectionRetryInterval_, connectionString_, true);
+  }
+  
+  
+  void OdbcIndex::ConfigureDatabase(DatabaseManager& manager)
+  {
+    uint32_t expectedVersion = 6;
+    
+    if (GetContext())   // "GetContext()" can possibly be NULL in the unit tests
+    {
+      expectedVersion = OrthancPluginGetExpectedDatabaseVersion(GetContext());
+    }
+
+    // Check the expected version of the database
+    if (expectedVersion != 6)
+    {
+      LOG(ERROR) << "This database plugin is incompatible with your version of Orthanc "
+                 << "expecting the DB schema version " << expectedVersion
+                 << ", but this plugin is only compatible with version 6";
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_Plugin);
+    }
+
+    OdbcDatabase& db = dynamic_cast<OdbcDatabase&>(manager.GetDatabase());
+
+    if (!db.DoesTableExist("resources"))
+    {
+      std::string sql;
+      Orthanc::EmbeddedResources::GetFileResource(sql, Orthanc::EmbeddedResources::ODBC_PREPARE_INDEX);
+
+      switch (db.GetDialect())
+      {
+        case Dialect_SQLite:
+          boost::replace_all(sql, "${LONGTEXT}", "TEXT");
+          boost::replace_all(sql, "${AUTOINCREMENT_TYPE}", "INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT");
+          boost::replace_all(sql, "${AUTOINCREMENT_INSERT}", "NULL, ");
+          break;
+        
+        case Dialect_PostgreSQL:
+          boost::replace_all(sql, "${LONGTEXT}", "TEXT");
+          boost::replace_all(sql, "${AUTOINCREMENT_TYPE}", "BIGSERIAL NOT NULL PRIMARY KEY");
+          boost::replace_all(sql, "${AUTOINCREMENT_INSERT}", "DEFAULT, ");
+          break;
+        
+        case Dialect_MySQL:
+          boost::replace_all(sql, "${LONGTEXT}", "LONGTEXT");
+          boost::replace_all(sql, "${AUTOINCREMENT_TYPE}", "BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY");
+          boost::replace_all(sql, "${AUTOINCREMENT_INSERT}", "NULL, ");
+          break;
+
+        case Dialect_MSSQL:
+          /**
+           * cf. OMSSQL-5: Use VARCHAR(MAX) instead of TEXT: (1)
+           * Microsoft issued a warning stating that "ntext, text, and
+           * image data types will be removed in a future version of
+           * SQL Server"
+           * (https://msdn.microsoft.com/en-us/library/ms187993.aspx),
+           * and (2) SQL Server does not support comparison of TEXT
+           * with '=' operator (e.g. in WHERE statements such as
+           * IndexBackend::LookupIdentifier())."
+           **/
+          boost::replace_all(sql, "${LONGTEXT}", "VARCHAR(MAX)");
+          boost::replace_all(sql, "${AUTOINCREMENT_TYPE}", "BIGINT IDENTITY NOT NULL PRIMARY KEY");
+          boost::replace_all(sql, "${AUTOINCREMENT_INSERT}", "");
+          break;
+
+        default:
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+      }
+
+      {
+        DatabaseManager::Transaction t(manager, TransactionType_ReadWrite);
+
+        db.ExecuteMultiLines(sql);
+
+        if (db.GetDialect() == Dialect_MySQL)
+        {
+          // Switch to the collation that is the default since MySQL
+          // 8.0.1. This must be *after* the creation of the tables.
+          db.ExecuteMultiLines("ALTER DATABASE CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
+        }
+
+        t.Commit();
+      }
+    }
+  }
+
+  
+  int64_t OdbcIndex::CreateResource(DatabaseManager& manager,
+                                    const char* publicId,
+                                    OrthancPluginResourceType type)
+  {
+    Dictionary args;
+    args.SetUtf8Value("id", publicId);
+    args.SetIntegerValue("type", static_cast<int>(type));
+    
+    switch (manager.GetDatabase().GetDialect())
+    {
+      case Dialect_SQLite:
+      {
+        {
+          DatabaseManager::CachedStatement statement(
+            STATEMENT_FROM_HERE, manager, "INSERT INTO Resources VALUES(NULL, ${type}, ${id}, NULL)");
+          
+          statement.SetParameterType("id", ValueType_Utf8String);
+          statement.SetParameterType("type", ValueType_Integer64);
+          statement.Execute(args);
+        }
+
+        // Must be out of the scope of "DatabaseManager::CachedStatement"
+        const int64_t id = GetSQLiteLastInsert(manager);
+        
+        if (type == OrthancPluginResourceType_Patient)
+        {
+          AddPatientToRecyclingOrder(manager, id);
+        }
+        
+        return id;
+      }
+      
+      case Dialect_PostgreSQL:
+      {
+        int64_t id;
+        
+        {
+          DatabaseManager::CachedStatement statement(
+            STATEMENT_FROM_HERE, manager,
+            "INSERT INTO Resources VALUES(DEFAULT, ${type}, ${id}, NULL) RETURNING internalId");
+          
+          statement.SetParameterType("id", ValueType_Utf8String);
+          statement.SetParameterType("type", ValueType_Integer64);
+          statement.Execute(args);
+          id = statement.ReadInteger64(0);
+        }
+        
+        if (type == OrthancPluginResourceType_Patient)
+        {
+          AddPatientToRecyclingOrder(manager, id);
+        }
+        
+        return id;
+      }
+        
+      case Dialect_MySQL:
+      {
+        {
+          DatabaseManager::CachedStatement statement(
+            STATEMENT_FROM_HERE, manager, "INSERT INTO Resources VALUES(NULL, ${type}, ${id}, NULL)");
+          
+          statement.SetParameterType("id", ValueType_Utf8String);
+          statement.SetParameterType("type", ValueType_Integer64);
+          statement.Execute(args);
+        }
+        
+        // Must be out of the scope of "DatabaseManager::CachedStatement"
+        const int64_t id = GetMySQLLastInsert(manager);
+        
+        if (type == OrthancPluginResourceType_Patient)
+        {
+          AddPatientToRecyclingOrder(manager, id);
+        }
+
+        return id;
+      }
+        
+      case Dialect_MSSQL:
+      {
+        {
+          DatabaseManager::CachedStatement statement(
+            STATEMENT_FROM_HERE, manager, "INSERT INTO Resources VALUES(${type}, ${id}, NULL)");
+          
+          statement.SetParameterType("id", ValueType_Utf8String);
+          statement.SetParameterType("type", ValueType_Integer64);
+          statement.Execute(args);
+        }
+        
+        // Must be out of the scope of "DatabaseManager::CachedStatement"
+        const int64_t id = GetMSSQLLastInsert(manager);
+
+        if (type == OrthancPluginResourceType_Patient)
+        {
+          AddPatientToRecyclingOrder(manager, id);
+        }
+
+        return id;
+      }
+
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+    }
+  }
+
+
+  void OdbcIndex::DeleteResource(IDatabaseBackendOutput& output,
+                                 DatabaseManager& manager,
+                                 int64_t id)
+  {
+    /**
+     * Contrarily to PostgreSQL and SQLite, the MySQL dialect
+     * doesn't support cascaded delete inside the same
+     * table. Furthermore, for maximum portability, we don't use
+     * triggers in the ODBC plugins. We therefore implement a custom
+     * version of this deletion.
+     **/
+
+    ClearDeletedFiles(manager);
+    ClearDeletedResources(manager);
+
+    OrthancPluginResourceType type;
+    bool hasParent;
+    int64_t parentId;
+
+    {
+      DatabaseManager::CachedStatement lookupResource(
+        STATEMENT_FROM_HERE, manager,
+        "SELECT resourceType, parentId FROM Resources WHERE internalId=${id}");
+      lookupResource.SetParameterType("id", ValueType_Integer64);
+      
+      Dictionary args;
+      args.SetIntegerValue("id", id);
+      lookupResource.Execute(args);
+      
+      if (lookupResource.IsDone())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource);
+      }
+      
+      type = static_cast<OrthancPluginResourceType>(lookupResource.ReadInteger32(0));
+      
+      if (lookupResource.GetResultField(1).GetType() == ValueType_Null)
+      {
+        hasParent = false;
+      }
+      else
+      {
+        hasParent = true;
+        parentId = lookupResource.ReadInteger64(1);
+      }
+    }
+
+    {
+      DatabaseManager::CachedStatement scheduleRootDeletion(
+        STATEMENT_FROM_HERE, manager,
+        "INSERT INTO DeletedResources SELECT internalId, resourceType, publicId "
+        "FROM Resources WHERE Resources.internalId = ${id}");
+      scheduleRootDeletion.SetParameterType("id", ValueType_Integer64);
+      
+      Dictionary args;
+      args.SetIntegerValue("id", id);
+      scheduleRootDeletion.Execute(args);
+    }
+
+    {
+      const std::string scheduleChildrenDeletion =
+        "INSERT INTO DeletedResources SELECT Resources.internalId, Resources.resourceType, Resources.publicId "
+        "FROM Resources INNER JOIN DeletedResources ON Resources.parentId = DeletedResources.internalId "
+        "WHERE Resources.resourceType = ${level}";
+      
+      switch (type)
+      {
+        /**
+         * WARNING: Don't add "break" or reorder cases below.
+         **/
+        
+        case OrthancPluginResourceType_Patient:
+        {
+          DatabaseManager::CachedStatement statement(STATEMENT_FROM_HERE, manager, scheduleChildrenDeletion);
+          statement.SetParameterType("level", ValueType_Integer64);
+          
+          Dictionary args;
+          args.SetIntegerValue("level", OrthancPluginResourceType_Study);
+          statement.Execute(args);
+        }
+        
+        case OrthancPluginResourceType_Study:
+        {
+          DatabaseManager::CachedStatement statement(STATEMENT_FROM_HERE, manager, scheduleChildrenDeletion);
+          statement.SetParameterType("level", ValueType_Integer64);
+          
+          Dictionary args;
+          args.SetIntegerValue("level", OrthancPluginResourceType_Series);
+          statement.Execute(args);
+        }
+        
+        case OrthancPluginResourceType_Series:
+        {
+          DatabaseManager::CachedStatement statement(STATEMENT_FROM_HERE, manager, scheduleChildrenDeletion);
+          statement.SetParameterType("level", ValueType_Integer64);
+
+          Dictionary args;
+          args.SetIntegerValue("level", OrthancPluginResourceType_Instance);
+          statement.Execute(args);
+        }
+        
+        case OrthancPluginResourceType_Instance:
+          // No child
+          break;
+
+        default:
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }
+    }
+
+    bool hasRemainingAncestor = false;
+    std::string remainingAncestor;
+    OrthancPluginResourceType ancestorType;
+    
+    if (hasParent)
+    {
+      int64_t currentAncestor = parentId;
+      int64_t currentResource = id;
+      OrthancPluginResourceType currentType = type;
+
+      for (;;)
+      {
+        bool hasSiblings;
+
+        {
+          std::string suffix;
+          if (manager.GetDialect() == Dialect_MSSQL)
+          {
+            suffix = "ORDER BY internalId OFFSET 0 ROWS FETCH FIRST 1 ROWS ONLY";
+          }
+          else
+          {
+            suffix = "LIMIT 1";
+          }
+
+          DatabaseManager::CachedStatement lookupSiblings(
+            STATEMENT_FROM_HERE, manager,
+            "SELECT internalId FROM Resources WHERE parentId = ${parent} AND internalId <> ${id} " + suffix);
+
+          lookupSiblings.SetParameterType("parent", ValueType_Integer64);
+          lookupSiblings.SetParameterType("id", ValueType_Integer64);
+
+          Dictionary args;
+          args.SetIntegerValue("parent", currentAncestor);
+          args.SetIntegerValue("id", currentResource);
+          lookupSiblings.Execute(args);
+
+          hasSiblings = !lookupSiblings.IsDone();
+        }
+
+        if (hasSiblings)
+        {
+          // There remains some sibling: Signal this remaining ancestor
+          hasRemainingAncestor = true;
+          remainingAncestor = GetPublicId(manager, currentAncestor);
+          ancestorType = GetParentType(currentType);
+          break;
+        }
+        else
+        {
+          // No sibling remaining: This parent resource must be deleted
+          {
+            DatabaseManager::CachedStatement addDeletedResource(
+              STATEMENT_FROM_HERE, manager,
+              "INSERT INTO DeletedResources SELECT internalId, resourceType, publicId "
+              "FROM Resources WHERE internalId=${id}");
+            addDeletedResource.SetParameterType("id", ValueType_Integer64);
+
+            Dictionary args;
+            args.SetIntegerValue("id", currentAncestor);
+            addDeletedResource.Execute(args);
+          }
+
+          int64_t tmp;
+          if (LookupParent(tmp, manager, currentAncestor))
+          {
+            currentResource = currentAncestor;
+            currentAncestor = tmp;
+            currentType = GetParentType(currentType);
+          }
+          else
+          {
+            assert(currentType == OrthancPluginResourceType_Study);
+            break;
+          }
+        }
+      }
+    }
+
+    {
+      // This is implemented by triggers in the PostgreSQL and MySQL plugins
+      DatabaseManager::CachedStatement lookupDeletedAttachments(
+        STATEMENT_FROM_HERE, manager,
+        "INSERT INTO DeletedFiles SELECT AttachedFiles.* FROM AttachedFiles "
+        "INNER JOIN DeletedResources ON AttachedFiles.id = DeletedResources.internalId");
+      lookupDeletedAttachments.Execute();
+    }
+
+    {
+      // Note that the attachments are automatically deleted by DELETE CASCADE
+      DatabaseManager::CachedStatement applyResourcesDeletion(
+        STATEMENT_FROM_HERE, manager,
+        "DELETE FROM Resources WHERE internalId IN (SELECT internalId FROM DeletedResources)");
+      applyResourcesDeletion.Execute();
+    }
+
+    SignalDeletedResources(output, manager);
+    SignalDeletedFiles(output, manager);
+    
+    if (hasRemainingAncestor)
+    {
+      assert(!remainingAncestor.empty());
+      output.SignalRemainingAncestor(remainingAncestor, ancestorType);
+    }
+  }
+
+
+  static void ExecuteLogChange(DatabaseManager::CachedStatement& statement,
+                               const Dictionary& args)
+  {
+    statement.SetParameterType("changeType", ValueType_Integer64);
+    statement.SetParameterType("id", ValueType_Integer64);
+    statement.SetParameterType("resourceType", ValueType_Integer64);
+    statement.SetParameterType("date", ValueType_Utf8String);
+    statement.Execute(args);
+  }
+  
+  
+  void OdbcIndex::LogChange(DatabaseManager& manager,
+                            int32_t changeType,
+                            int64_t resourceId,
+                            OrthancPluginResourceType resourceType,
+                            const char* date)
+  {
+    Dictionary args;
+    args.SetIntegerValue("changeType", changeType);
+    args.SetIntegerValue("id", resourceId);
+    args.SetIntegerValue("resourceType", resourceType);
+    args.SetUtf8Value("date", date);
+
+    int64_t seq;
+
+    switch (manager.GetDatabase().GetDialect())
+    {
+      case Dialect_SQLite:
+      {
+        DatabaseManager::CachedStatement statement(
+          STATEMENT_FROM_HERE, manager,
+          "INSERT INTO Changes VALUES(NULL, ${changeType}, ${id}, ${resourceType}, ${date})");
+        ExecuteLogChange(statement, args);
+        seq = GetSQLiteLastInsert(manager);
+        break;
+      }
+      
+      case Dialect_PostgreSQL:
+      {
+        DatabaseManager::CachedStatement statement(
+          STATEMENT_FROM_HERE, manager,
+          "INSERT INTO Changes VALUES(DEFAULT, ${changeType}, ${id}, ${resourceType}, ${date}) RETURNING seq");
+        ExecuteLogChange(statement, args);
+        seq = statement.ReadInteger64(0);
+        break;
+      }
+        
+      case Dialect_MySQL:
+      {
+        DatabaseManager::CachedStatement statement(
+          STATEMENT_FROM_HERE, manager,
+          "INSERT INTO Changes VALUES(NULL, ${changeType}, ${id}, ${resourceType}, ${date})");
+        ExecuteLogChange(statement, args);
+        seq = GetMySQLLastInsert(manager);
+        break;
+      }
+        
+      case Dialect_MSSQL:
+      {
+        DatabaseManager::CachedStatement statement(
+          STATEMENT_FROM_HERE, manager,
+          "INSERT INTO Changes VALUES(${changeType}, ${id}, ${resourceType}, ${date})");
+        ExecuteLogChange(statement, args);
+        seq = GetMSSQLLastInsert(manager);
+        break;
+      }
+
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+    }
+    
+    std::string value = boost::lexical_cast<std::string>(seq);
+    SetGlobalProperty(manager, MISSING_SERVER_IDENTIFIER, GlobalProperty_LastChange, value.c_str());
+  }
+
+
+  int64_t OdbcIndex::GetLastChangeIndex(DatabaseManager& manager)
+  {
+    std::string value;
+    
+    if (LookupGlobalProperty(value, manager, MISSING_SERVER_IDENTIFIER, GlobalProperty_LastChange))
+    {
+      return boost::lexical_cast<int64_t>(value);
+    }
+    else
+    {
+      return 0;
+    }
+  }
+
+
+  void OdbcIndex::DeleteAttachment(IDatabaseBackendOutput& output,
+                                   DatabaseManager& manager,
+                                   int64_t id,
+                                   int32_t attachment)
+  {
+    ClearDeletedFiles(manager);
+
+    Dictionary args;
+    args.SetIntegerValue("id", id);
+    args.SetIntegerValue("type", static_cast<int>(attachment));
+    
+    {
+      // This is implemented by triggers in the PostgreSQL and MySQL plugins
+      DatabaseManager::CachedStatement statement(
+        STATEMENT_FROM_HERE, manager,
+        "INSERT INTO DeletedFiles SELECT * FROM AttachedFiles WHERE id=${id} AND fileType=${type}");
+      
+      statement.SetParameterType("id", ValueType_Integer64);
+      statement.SetParameterType("type", ValueType_Integer64);
+      statement.Execute(args);
+    }
+
+    {
+      DatabaseManager::CachedStatement statement(
+        STATEMENT_FROM_HERE, manager,
+        "DELETE FROM AttachedFiles WHERE id=${id} AND fileType=${type}");
+
+      statement.SetParameterType("id", ValueType_Integer64);
+      statement.SetParameterType("type", ValueType_Integer64);
+      statement.Execute(args);
+    }
+
+    SignalDeletedFiles(output, manager);
+  }
+}