changeset 705:435ad957f829 sql-opti

merged default -> sql-opti
author Alain Mazy <am@orthanc.team>
date Fri, 27 Jun 2025 15:29:59 +0200
parents bcea50e40d6e (diff) 1f0d265f24d9 (current diff)
children 2bf70b3ef0d5
files Framework/Common/DatabaseManager.cpp Framework/Common/DatabaseManager.h Framework/Plugins/DatabaseBackendAdapterV4.cpp Framework/Plugins/DatabaseBackendAdapterV4.h Framework/Plugins/IndexBackend.cpp Framework/Plugins/IndexBackend.h Framework/Plugins/IndexUnitTests.h PostgreSQL/CMakeLists.txt PostgreSQL/NEWS PostgreSQL/Plugins/IndexPlugin.cpp PostgreSQL/Plugins/PostgreSQLIndex.cpp PostgreSQL/Plugins/PostgreSQLIndex.h PostgreSQL/Plugins/SQL/Downgrades/Rev499ToRev4.sql PostgreSQL/Plugins/SQL/Downgrades/Rev599ToRev5.sql PostgreSQL/Plugins/SQL/PrepareIndex.sql PostgreSQL/Plugins/SQL/Upgrades/Rev4ToRev499.sql PostgreSQL/Plugins/SQL/Upgrades/Rev5ToRev599.sql Resources/CMake/DatabasesPluginConfiguration.cmake
diffstat 23 files changed, 1440 insertions(+), 371 deletions(-) [+]
line wrap: on
line diff
--- a/Framework/Common/DatabaseManager.cpp	Tue Jun 17 12:41:37 2025 +0200
+++ b/Framework/Common/DatabaseManager.cpp	Fri Jun 27 15:29:59 2025 +0200
@@ -160,7 +160,9 @@
     
   DatabaseManager::DatabaseManager(IDatabaseFactory* factory) :
     factory_(factory),
-    dialect_(Dialect_Unknown)
+    dialect_(Dialect_Unknown),
+    creationTime_(boost::posix_time::second_clock::universal_time()),
+    lastUseTime_(boost::posix_time::second_clock::universal_time())
   {
     if (factory == NULL)
     {
@@ -168,6 +170,20 @@
     }
   }
 
+  uint64_t DatabaseManager::GetElapsedSecondsSinceCreation() const
+  {
+    boost::posix_time::ptime now = boost::posix_time::second_clock::universal_time();
+    boost::posix_time::time_duration diff = now - creationTime_;
+    return static_cast<uint64_t>(diff.total_seconds());    
+  }
+
+  uint64_t DatabaseManager::GetElapsedSecondsSinceLastUse() const
+  {
+    boost::posix_time::ptime now = boost::posix_time::second_clock::universal_time();
+    boost::posix_time::time_duration diff = now - lastUseTime_;
+    return static_cast<uint64_t>(diff.total_seconds());    
+  }
+
   
   IDatabase& DatabaseManager::GetDatabase()
   {
@@ -218,6 +234,8 @@
       }
 
       transaction_.reset(GetDatabase().CreateTransaction(type));
+      
+      lastUseTime_= boost::posix_time::second_clock::universal_time();
     }
     catch (Orthanc::OrthancException& e)
     {
--- a/Framework/Common/DatabaseManager.h	Tue Jun 17 12:41:37 2025 +0200
+++ b/Framework/Common/DatabaseManager.h	Fri Jun 27 15:29:59 2025 +0200
@@ -30,6 +30,7 @@
 #include <Enumerations.h>
 
 #include <memory>
+#include <boost/date_time/posix_time/posix_time.hpp>
 
 
 namespace OrthancDatabases
@@ -56,6 +57,8 @@
     std::unique_ptr<ITransaction>  transaction_;
     CachedStatements               cachedStatements_;
     Dialect                        dialect_;
+    boost::posix_time::ptime       creationTime_;
+    boost::posix_time::ptime       lastUseTime_;
 
     void CloseIfUnavailable(Orthanc::ErrorCode e);
 
@@ -88,6 +91,9 @@
     
     void RollbackTransaction();
 
+    uint64_t GetElapsedSecondsSinceCreation() const;
+
+    uint64_t GetElapsedSecondsSinceLastUse() const;
 
     // This class is only used in the "StorageBackend" and in
     // "IDatabaseBackend::ConfigureDatabase()"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Plugins/BaseIndexConnectionsPool.cpp	Fri Jun 27 15:29:59 2025 +0200
@@ -0,0 +1,177 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, 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 "BaseIndexConnectionsPool.h"
+
+#include <Logging.h>
+
+
+namespace OrthancDatabases
+{
+//   class BaseIndexConnectionsPool::ManagerReference : public Orthanc::IDynamicObject
+//   {
+//   private:
+//     DatabaseManager*  manager_;
+
+//   public:
+//     explicit ManagerReference(DatabaseManager& manager) :
+//       manager_(&manager)
+//     {
+//     }
+
+//     DatabaseManager& GetManager()
+//     {
+//       assert(manager_ != NULL);
+//       return *manager_;
+//     }
+//   };
+
+
+  void BaseIndexConnectionsPool::HousekeepingThread(BaseIndexConnectionsPool* that)
+  {
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 2)
+    OrthancPluginSetCurrentThreadName(OrthancPlugins::GetGlobalContext(), "DB HOUSEKEEPING");    
+#endif
+
+    boost::posix_time::ptime lastInvocation = boost::posix_time::second_clock::local_time();
+
+    while (that->housekeepingContinue_)
+    {
+      if (boost::posix_time::second_clock::local_time() - lastInvocation >= that->housekeepingDelay_)
+      {
+        try
+        {
+          {
+            Accessor accessor(*that);
+            accessor.GetBackend().PerformDbHousekeeping(accessor.GetManager());
+          }
+
+          that->PerformPoolHousekeeping();
+        }
+        catch (Orthanc::OrthancException& e)
+        {
+          LOG(ERROR) << "Exception during the database housekeeping: " << e.What();
+        }
+        catch (...)
+        {
+          LOG(ERROR) << "Native exception during the database houskeeping";
+        }
+
+        lastInvocation = boost::posix_time::second_clock::local_time();
+      }
+
+      boost::this_thread::sleep(boost::posix_time::milliseconds(1000));
+    }
+  }
+
+
+  BaseIndexConnectionsPool::BaseIndexConnectionsPool(IndexBackend* backend,
+                                                     unsigned int houseKeepingDelaySeconds) :
+    backend_(backend),
+    housekeepingContinue_(true),
+    housekeepingDelay_(boost::posix_time::seconds(houseKeepingDelaySeconds))
+  {
+    if (backend == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+    }
+    else if (backend->HasPerformDbHousekeeping() &&
+             houseKeepingDelaySeconds == 0)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange,
+                                      "The delay between two executions of housekeeping cannot be zero second");
+    }
+    else
+    {
+      context_ = backend_->GetContext();
+    }
+  }
+
+  
+  BaseIndexConnectionsPool::~BaseIndexConnectionsPool()
+  {
+  }
+
+
+  void BaseIndexConnectionsPool::StartHousekeepingThread()
+  {
+    housekeepingContinue_ = true;
+
+    if (backend_->HasPerformDbHousekeeping())
+    {
+      housekeepingThread_ = boost::thread(HousekeepingThread, this);
+    }
+  }
+
+  void BaseIndexConnectionsPool::StopHousekeepingThread()
+  {
+    housekeepingContinue_ = false;
+    if (housekeepingThread_.joinable())
+    {
+      housekeepingThread_.join();
+    }
+  }
+
+
+  BaseIndexConnectionsPool::Accessor::Accessor(BaseIndexConnectionsPool& pool) :
+    // lock_(pool.connectionsMutex_),
+    pool_(pool),
+    manager_(NULL)
+  {
+    for (;;)
+    {
+      std::unique_ptr<DatabaseManager> manager(pool.GetConnection());
+      if (manager.get() != NULL)
+      {
+        manager_ = manager.release();
+        return;
+      }
+      boost::this_thread::sleep(boost::posix_time::millisec(100));
+    }
+  }
+
+  
+  BaseIndexConnectionsPool::Accessor::~Accessor()
+  {
+    assert(manager_ != NULL);
+    pool_.ReleaseConnection(manager_);
+    // boost::unique_lock<boost::shared_mutex>  lock(pool_.connectionsMutex_);
+    // pool_.availableConnections_.push_front(manager_);
+    // pool_.availableConnectionsSemaphore_.Release(1);
+
+
+  }
+
+  
+  IndexBackend& BaseIndexConnectionsPool::Accessor::GetBackend() const
+  {
+    return *pool_.backend_;
+  }
+
+  
+  DatabaseManager& BaseIndexConnectionsPool::Accessor::GetManager() const
+  {
+    assert(manager_ != NULL);
+    return *manager_;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Plugins/BaseIndexConnectionsPool.h	Fri Jun 27 15:29:59 2025 +0200
@@ -0,0 +1,94 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, 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/>.
+ **/
+
+
+#pragma once
+
+#include "IdentifierTag.h"
+#include "IndexBackend.h"
+
+#include <MultiThreading/SharedMessageQueue.h>
+#include <MultiThreading/Semaphore.h>
+
+#include <list>
+#include <boost/thread.hpp>
+
+namespace OrthancDatabases
+{
+  class BaseIndexConnectionsPool : public boost::noncopyable
+  {
+  protected:
+
+    std::unique_ptr<IndexBackend>  backend_;
+    OrthancPluginContext*          context_;
+    boost::mutex                   connectionsMutex_;
+
+    bool                           housekeepingContinue_;
+    boost::thread                  housekeepingThread_;
+    boost::posix_time::time_duration  housekeepingDelay_;
+
+    static void HousekeepingThread(BaseIndexConnectionsPool* that);
+
+    virtual void PerformPoolHousekeeping()
+    {}
+
+    void StartHousekeepingThread();
+
+    void StopHousekeepingThread();
+
+    virtual DatabaseManager* GetConnection() = 0;
+    
+    virtual void ReleaseConnection(DatabaseManager* manager) = 0;
+
+  public:
+    BaseIndexConnectionsPool(IndexBackend* backend /* takes ownership */,
+                             unsigned int houseKeepingDelaySeconds);
+
+    virtual ~BaseIndexConnectionsPool();
+
+    OrthancPluginContext* GetContext() const
+    {
+      return context_;
+    }
+
+    virtual void OpenConnections(bool hasIdentifierTags,
+                                 const std::list<IdentifierTag>& identifierTags) = 0;
+
+    virtual void CloseConnections() = 0;
+
+    class Accessor : public boost::noncopyable
+    {
+    private:
+      BaseIndexConnectionsPool&                pool_;
+      DatabaseManager*                         manager_;
+      
+    public:
+      explicit Accessor(BaseIndexConnectionsPool& pool);
+
+      ~Accessor();
+
+      IndexBackend& GetBackend() const;
+
+      DatabaseManager& GetManager() const;
+    };
+  };
+}
--- a/Framework/Plugins/DatabaseBackendAdapterV4.cpp	Tue Jun 17 12:41:37 2025 +0200
+++ b/Framework/Plugins/DatabaseBackendAdapterV4.cpp	Fri Jun 27 15:29:59 2025 +0200
@@ -26,6 +26,7 @@
 #if defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE)         // Macro introduced in Orthanc 1.3.1
 #  if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 0)
 
+#include "DynamicIndexConnectionsPool.h"
 #include "IndexConnectionsPool.h"
 #include "MessagesToolbox.h"
 
@@ -431,13 +432,13 @@
 
   static void ProcessDatabaseOperation(Orthanc::DatabasePluginMessages::DatabaseResponse& response,
                                        const Orthanc::DatabasePluginMessages::DatabaseRequest& request,
-                                       IndexConnectionsPool& pool)
+                                       BaseIndexConnectionsPool& pool)
   {
     switch (request.operation())
     {
       case Orthanc::DatabasePluginMessages::OPERATION_GET_SYSTEM_INFORMATION:
       {
-        IndexConnectionsPool::Accessor accessor(pool);
+        BaseIndexConnectionsPool::Accessor accessor(pool);
         response.mutable_get_system_information()->set_database_version(accessor.GetBackend().GetDatabaseVersion(accessor.GetManager()));
         response.mutable_get_system_information()->set_supports_flush_to_disk(false);
         response.mutable_get_system_information()->set_supports_revisions(accessor.GetBackend().HasRevisionsSupport());
@@ -502,7 +503,7 @@
 
       case Orthanc::DatabasePluginMessages::OPERATION_START_TRANSACTION:
       {
-        std::unique_ptr<IndexConnectionsPool::Accessor> transaction(new IndexConnectionsPool::Accessor(pool));
+        std::unique_ptr<BaseIndexConnectionsPool::Accessor> transaction(new BaseIndexConnectionsPool::Accessor(pool));
 
         switch (request.start_transaction().type())
         {
@@ -524,7 +525,7 @@
         
       case Orthanc::DatabasePluginMessages::OPERATION_UPGRADE:
       {
-        IndexConnectionsPool::Accessor accessor(pool);
+        BaseIndexConnectionsPool::Accessor accessor(pool);
         OrthancPluginStorageArea* storageArea = reinterpret_cast<OrthancPluginStorageArea*>(request.upgrade().storage_area());
         accessor.GetBackend().UpgradeDatabase(accessor.GetManager(), request.upgrade().target_version(), storageArea);
         break;
@@ -532,7 +533,7 @@
               
       case Orthanc::DatabasePluginMessages::OPERATION_FINALIZE_TRANSACTION:
       {
-        IndexConnectionsPool::Accessor* transaction = reinterpret_cast<IndexConnectionsPool::Accessor*>(request.finalize_transaction().transaction());
+        BaseIndexConnectionsPool::Accessor* transaction = reinterpret_cast<BaseIndexConnectionsPool::Accessor*>(request.finalize_transaction().transaction());
         
         if (transaction == NULL)
         {
@@ -549,7 +550,7 @@
 #if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 3)
       case Orthanc::DatabasePluginMessages::OPERATION_MEASURE_LATENCY:
       {
-        IndexConnectionsPool::Accessor accessor(pool);
+        BaseIndexConnectionsPool::Accessor accessor(pool);
         response.mutable_measure_latency()->set_latency_us(accessor.GetBackend().MeasureLatency(accessor.GetManager()));
         break;
       }
@@ -1430,7 +1431,7 @@
       return OrthancPluginErrorCode_InternalError;
     }
 
-    IndexConnectionsPool& pool = *reinterpret_cast<IndexConnectionsPool*>(rawPool);
+    BaseIndexConnectionsPool& pool = *reinterpret_cast<BaseIndexConnectionsPool*>(rawPool);
 
     try
     {
@@ -1444,7 +1445,7 @@
           
         case Orthanc::DatabasePluginMessages::REQUEST_TRANSACTION:
         {
-          IndexConnectionsPool::Accessor& transaction = *reinterpret_cast<IndexConnectionsPool::Accessor*>(request.transaction_request().transaction());
+          BaseIndexConnectionsPool::Accessor& transaction = *reinterpret_cast<BaseIndexConnectionsPool::Accessor*>(request.transaction_request().transaction());
           ProcessTransactionOperation(*response.mutable_transaction_response(), request.transaction_request(),
                                       transaction.GetBackend(), transaction.GetManager());
           break;
@@ -1502,7 +1503,7 @@
   {
     if (rawPool != NULL)
     {
-      IndexConnectionsPool* pool = reinterpret_cast<IndexConnectionsPool*>(rawPool);
+      BaseIndexConnectionsPool* pool = reinterpret_cast<BaseIndexConnectionsPool*>(rawPool);
       
       if (isBackendInUse_)
       {
@@ -1524,10 +1525,20 @@
   
   void DatabaseBackendAdapterV4::Register(IndexBackend* backend,
                                           size_t countConnections,
+                                          bool useDynamicConnectionPool,
                                           unsigned int maxDatabaseRetries,
                                           unsigned int housekeepingDelaySeconds)
   {
-    std::unique_ptr<IndexConnectionsPool> pool(new IndexConnectionsPool(backend, countConnections, housekeepingDelaySeconds));
+    std::unique_ptr<BaseIndexConnectionsPool> pool;
+
+    if (useDynamicConnectionPool)
+    {
+      pool.reset(new DynamicIndexConnectionsPool(backend, countConnections, housekeepingDelaySeconds));
+    }
+    else
+    {
+      pool.reset(new IndexConnectionsPool(backend, countConnections, housekeepingDelaySeconds));
+    }
     
     if (isBackendInUse_)
     {
--- a/Framework/Plugins/DatabaseBackendAdapterV4.h	Tue Jun 17 12:41:37 2025 +0200
+++ b/Framework/Plugins/DatabaseBackendAdapterV4.h	Fri Jun 27 15:29:59 2025 +0200
@@ -50,6 +50,7 @@
   public:
     static void Register(IndexBackend* backend,
                          size_t countConnections,
+                         bool useDynamicConnectionPool,
                          unsigned int maxDatabaseRetries,
                          unsigned int housekeepingDelaySeconds);
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Plugins/DynamicIndexConnectionsPool.cpp	Fri Jun 27 15:29:59 2025 +0200
@@ -0,0 +1,198 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, 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 "DynamicIndexConnectionsPool.h"
+
+#include <Logging.h>
+
+
+namespace OrthancDatabases
+{
+  void DynamicIndexConnectionsPool::PerformPoolHousekeeping()
+  {
+    CleanupOldConnections();
+
+    OrthancPluginSetMetricsValue(OrthancPlugins::GetGlobalContext(), "orthanc_index_active_connections_count", maxConnectionsCount_ - connectionsSemaphore_.GetAvailableResourcesCount(), OrthancPluginMetricsType_Default);
+  }
+
+
+  DynamicIndexConnectionsPool::DynamicIndexConnectionsPool(IndexBackend* backend,
+                                                           size_t maxConnectionsCount,
+                                                           unsigned int houseKeepingDelaySeconds) :
+    BaseIndexConnectionsPool(backend, houseKeepingDelaySeconds),
+    maxConnectionsCount_(maxConnectionsCount),
+    connectionsSemaphore_(maxConnectionsCount),
+    availableConnectionsSemaphore_(0)
+  {
+    if (maxConnectionsCount == 0)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange,
+                                      "There must be a non-zero number of connections to the database");
+    }
+  }
+
+  
+  DynamicIndexConnectionsPool::~DynamicIndexConnectionsPool()
+  {
+    boost::mutex::scoped_lock  lock(connectionsMutex_);
+
+    for (std::list<DatabaseManager*>::iterator
+           it = connections_.begin(); it != connections_.end(); ++it)
+    {
+      assert(*it != NULL);
+      delete *it;
+    }
+  }
+
+
+  void DynamicIndexConnectionsPool::OpenConnections(bool hasIdentifierTags,
+                                             const std::list<IdentifierTag>& identifierTags)
+  {
+    assert(backend_.get() != NULL);
+
+    DynamicIndexConnectionsPool::Accessor accessor(*this);
+    backend_->ConfigureDatabase(accessor.GetManager(), hasIdentifierTags, identifierTags);
+
+    StartHousekeepingThread();
+  }
+
+
+  void DynamicIndexConnectionsPool::CloseConnections()
+  {
+    StopHousekeepingThread();
+
+    boost::mutex::scoped_lock  lock(connectionsMutex_);
+
+    for (std::list<DatabaseManager*>::iterator
+            it = connections_.begin(); it != connections_.end(); ++it)
+    {
+      assert(*it != NULL);
+      (*it)->Close();
+    }
+  }
+
+  DatabaseManager* DynamicIndexConnectionsPool::GetConnection()
+  {
+    if (availableConnectionsSemaphore_.TryAcquire(1)) // there is a connection directly available, take it
+    {
+      // LOG(INFO) << "--- Reusing an available connection";
+
+      boost::mutex::scoped_lock  lock(connectionsMutex_);
+  
+      std::unique_ptr<DatabaseManager> manager(availableConnections_.front());
+      availableConnections_.pop_front();
+      return manager.release();
+    }
+    else if (connectionsSemaphore_.TryAcquire(1))  // no connection directly available, check if we can create a new one
+    {
+      // LOG(INFO) << "--- Creating a new connection";
+
+      boost::mutex::scoped_lock  lock(connectionsMutex_);
+      connections_.push_back(new DatabaseManager(backend_->CreateDatabaseFactory()));
+      connections_.back()->GetDatabase();  // Make sure to open the database connection
+
+      // no need to push it in the availableConnections since it is being used immediately
+      return connections_.back();
+    }
+    else // unable to get a connection now
+    {
+      return NULL;
+    }
+
+  }
+
+  void DynamicIndexConnectionsPool::CleanupOldConnections()
+  {
+    boost::mutex::scoped_lock  lock(connectionsMutex_);
+
+    while (availableConnectionsSemaphore_.TryAcquire(1))
+    {
+      DatabaseManager* manager = availableConnections_.front();
+      if (manager->GetElapsedSecondsSinceLastUse() > 60 || manager->GetElapsedSecondsSinceCreation() > 3600)
+      {
+        // LOG(INFO) << "--- Deleting an old connection";
+
+        availableConnections_.pop_front();
+        connections_.remove(manager);      
+        
+        delete manager;
+        connectionsSemaphore_.Release(1);
+      }
+      else
+      {
+        availableConnectionsSemaphore_.Release(1);  // we have not consumed it
+        break;
+      }
+    }
+
+  }
+
+  void DynamicIndexConnectionsPool::ReleaseConnection(DatabaseManager* manager)
+  {
+    boost::mutex::scoped_lock  lock(connectionsMutex_);
+    availableConnections_.push_front(manager);
+    availableConnectionsSemaphore_.Release(1);
+  }
+
+  // DynamicIndexConnectionsPool::Accessor::Accessor(DynamicIndexConnectionsPool& pool) :
+  //   // lock_(pool.connectionsMutex_),
+  //   pool_(pool),
+  //   manager_(NULL)
+  // {
+  //   for (;;)
+  //   {
+  //     std::unique_ptr<DatabaseManager> manager(pool.GetConnection());
+  //     if (manager.get() != NULL)
+  //     {
+  //       manager_ = manager.release();
+  //       return;
+  //     }
+  //     boost::this_thread::sleep(boost::posix_time::millisec(100));
+  //   }
+  // }
+
+  
+  // DynamicIndexConnectionsPool::Accessor::~Accessor()
+  // {
+  //   assert(manager_ != NULL);
+  //   pool_.ReleaseConnection(manager_);
+  //   // boost::unique_lock<boost::shared_mutex>  lock(pool_.connectionsMutex_);
+  //   // pool_.availableConnections_.push_front(manager_);
+  //   // pool_.availableConnectionsSemaphore_.Release(1);
+
+
+  // }
+
+  
+  // IndexBackend& DynamicIndexConnectionsPool::Accessor::GetBackend() const
+  // {
+  //   return *pool_.backend_;
+  // }
+
+  
+  // DatabaseManager& DynamicIndexConnectionsPool::Accessor::GetManager() const
+  // {
+  //   assert(manager_ != NULL);
+  //   return *manager_;
+  // }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Plugins/DynamicIndexConnectionsPool.h	Fri Jun 27 15:29:59 2025 +0200
@@ -0,0 +1,71 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, 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/>.
+ **/
+
+
+#pragma once
+
+#include "IdentifierTag.h"
+#include "IndexBackend.h"
+#include "BaseIndexConnectionsPool.h"
+
+#include <MultiThreading/SharedMessageQueue.h>
+#include <MultiThreading/Semaphore.h>
+
+#include <list>
+#include <boost/thread.hpp>
+
+namespace OrthancDatabases
+{
+  class DynamicIndexConnectionsPool : public BaseIndexConnectionsPool
+  {
+  private:
+    boost::mutex                   connectionsMutex_;
+    size_t                         maxConnectionsCount_;
+    Orthanc::Semaphore             connectionsSemaphore_;
+    std::list<DatabaseManager*>    connections_;
+    Orthanc::Semaphore             availableConnectionsSemaphore_;
+    std::list<DatabaseManager*>    availableConnections_;
+
+    void CleanupOldConnections();
+
+  protected:
+
+    virtual DatabaseManager* GetConnection() ORTHANC_OVERRIDE;
+    
+    virtual void ReleaseConnection(DatabaseManager* manager) ORTHANC_OVERRIDE;
+
+    virtual void PerformPoolHousekeeping() ORTHANC_OVERRIDE;
+
+  public:
+    DynamicIndexConnectionsPool(IndexBackend* backend /* takes ownership */,
+                                size_t maxConnectionsCount,
+                                unsigned int houseKeepingDelaySeconds);
+
+    virtual ~DynamicIndexConnectionsPool();
+
+    virtual void OpenConnections(bool hasIdentifierTags,
+                                 const std::list<IdentifierTag>& identifierTags) ORTHANC_OVERRIDE;
+
+    virtual void CloseConnections() ORTHANC_OVERRIDE;
+
+  };
+}
--- a/Framework/Plugins/IndexBackend.cpp	Tue Jun 17 12:41:37 2025 +0200
+++ b/Framework/Plugins/IndexBackend.cpp	Fri Jun 27 15:29:59 2025 +0200
@@ -2069,9 +2069,7 @@
         break;
         
       case Dialect_PostgreSQL:
-        statement.reset(new DatabaseManager::CachedStatement(
-                          STATEMENT_FROM_HERE, manager,
-                          "SELECT CAST(COUNT(*) AS BIGINT) FROM PatientRecyclingOrder"));
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
         break;
 
       case Dialect_MSSQL:
@@ -3024,6 +3022,7 @@
   
   void IndexBackend::Register(IndexBackend* backend,
                               size_t countConnections,
+                              bool useDynamicConnectionPool,
                               unsigned int maxDatabaseRetries,
                               unsigned int housekeepingDelaySeconds)
   {
@@ -3039,7 +3038,7 @@
 #  if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 0)
     if (OrthancPluginCheckVersionAdvanced(backend->GetContext(), 1, 12, 0) == 1)
     {
-      DatabaseBackendAdapterV4::Register(backend, countConnections, maxDatabaseRetries, housekeepingDelaySeconds);
+      DatabaseBackendAdapterV4::Register(backend, countConnections, useDynamicConnectionPool, maxDatabaseRetries, housekeepingDelaySeconds);
       return;
     }
 #  endif
--- a/Framework/Plugins/IndexBackend.h	Tue Jun 17 12:41:37 2025 +0200
+++ b/Framework/Plugins/IndexBackend.h	Fri Jun 27 15:29:59 2025 +0200
@@ -539,6 +539,7 @@
      **/
     static void Register(IndexBackend* backend,
                          size_t countConnections,
+                         bool useDynamicConnectionPool,
                          unsigned int maxDatabaseRetries,
                          unsigned int housekeepingDelaySeconds);
 
--- a/Framework/Plugins/IndexConnectionsPool.cpp	Tue Jun 17 12:41:37 2025 +0200
+++ b/Framework/Plugins/IndexConnectionsPool.cpp	Fri Jun 27 15:29:59 2025 +0200
@@ -47,63 +47,17 @@
   };
 
 
-  void IndexConnectionsPool::HousekeepingThread(IndexConnectionsPool* that)
-  {
-#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 2)
-    OrthancPluginSetCurrentThreadName(OrthancPlugins::GetGlobalContext(), "DB HOUSEKEEPING");    
-#endif
-
-    boost::posix_time::ptime lastInvocation = boost::posix_time::second_clock::local_time();
-
-    while (that->housekeepingContinue_)
-    {
-      if (boost::posix_time::second_clock::local_time() - lastInvocation >= that->housekeepingDelay_)
-      {
-        try
-        {
-          Accessor accessor(*that);
-          accessor.GetBackend().PerformDbHousekeeping(accessor.GetManager());
-        }
-        catch (Orthanc::OrthancException& e)
-        {
-          LOG(ERROR) << "Exception during the database housekeeping: " << e.What();
-        }
-        catch (...)
-        {
-          LOG(ERROR) << "Native exception during the database houskeeping";
-        }
-
-        lastInvocation = boost::posix_time::second_clock::local_time();
-      }
-
-      boost::this_thread::sleep(boost::posix_time::milliseconds(1000));
-    }
-  }
-
-
   IndexConnectionsPool::IndexConnectionsPool(IndexBackend* backend,
                                              size_t countConnections,
                                              unsigned int houseKeepingDelaySeconds) :
-    backend_(backend),
-    countConnections_(countConnections),
-    housekeepingContinue_(true),
-    housekeepingDelay_(boost::posix_time::seconds(houseKeepingDelaySeconds))
+    BaseIndexConnectionsPool(backend, houseKeepingDelaySeconds),
+    countConnections_(countConnections)
   {
     if (countConnections == 0)
     {
       throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange,
                                       "There must be a non-zero number of connections to the database");
     }
-    else if (backend == NULL)
-    {
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
-    }
-    else if (backend->HasPerformDbHousekeeping() &&
-             houseKeepingDelaySeconds == 0)
-    {
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange,
-                                      "The delay between two executions of housekeeping cannot be zero second");
-    }
     else
     {
       context_ = backend_->GetContext();
@@ -122,6 +76,13 @@
   }
 
 
+  void IndexConnectionsPool::PerformPoolHousekeeping()
+  {
+    // this is actually a fixed value !
+    OrthancPluginSetMetricsValue(OrthancPlugins::GetGlobalContext(), "orthanc_index_active_connections_count", countConnections_, OrthancPluginMetricsType_Default);
+  }
+
+
   void IndexConnectionsPool::OpenConnections(bool hasIdentifierTags,
                                              const std::list<IdentifierTag>& identifierTags)
   {
@@ -152,13 +113,7 @@
         availableConnections_.Enqueue(new ManagerReference(**it));
       }
 
-      // Start the housekeeping thread
-      housekeepingContinue_ = true;
-
-      if (backend_->HasPerformDbHousekeeping())
-      {
-        housekeepingThread_ = boost::thread(HousekeepingThread, this);
-      }
+      StartHousekeepingThread();
     }
     else
     {
@@ -169,14 +124,7 @@
 
   void IndexConnectionsPool::CloseConnections()
   {
-    {
-      // Stop the housekeeping thread
-      housekeepingContinue_ = false;
-      if (housekeepingThread_.joinable())
-      {
-        housekeepingThread_.join();
-      }
-    }
+    StopHousekeepingThread();
 
     boost::unique_lock<boost::shared_mutex>  lock(connectionsMutex_);
 
@@ -199,40 +147,20 @@
     }
   }
 
-
-  IndexConnectionsPool::Accessor::Accessor(IndexConnectionsPool& pool) :
-    lock_(pool.connectionsMutex_),
-    pool_(pool),
-    manager_(NULL)
+  DatabaseManager* IndexConnectionsPool::GetConnection()
   {
-    for (;;)
+    std::unique_ptr<Orthanc::IDynamicObject> manager(availableConnections_.Dequeue(1));
+    if (manager.get() != NULL)
     {
-      std::unique_ptr<Orthanc::IDynamicObject> manager(pool.availableConnections_.Dequeue(100));
-      if (manager.get() != NULL)
-      {
-        manager_ = &dynamic_cast<ManagerReference&>(*manager).GetManager();
-        return;
-      }
+      return &dynamic_cast<ManagerReference&>(*manager).GetManager();
     }
+    return NULL;
   }
 
-  
-  IndexConnectionsPool::Accessor::~Accessor()
+  void IndexConnectionsPool::ReleaseConnection(DatabaseManager* manager)
   {
-    assert(manager_ != NULL);
-    pool_.availableConnections_.Enqueue(new ManagerReference(*manager_));
+    assert(manager != NULL);
+    availableConnections_.Enqueue(new ManagerReference(*manager));
   }
 
-  
-  IndexBackend& IndexConnectionsPool::Accessor::GetBackend() const
-  {
-    return *pool_.backend_;
-  }
-
-  
-  DatabaseManager& IndexConnectionsPool::Accessor::GetManager() const
-  {
-    assert(manager_ != NULL);
-    return *manager_;
-  }
 }
--- a/Framework/Plugins/IndexConnectionsPool.h	Tue Jun 17 12:41:37 2025 +0200
+++ b/Framework/Plugins/IndexConnectionsPool.h	Fri Jun 27 15:29:59 2025 +0200
@@ -25,6 +25,7 @@
 
 #include "IdentifierTag.h"
 #include "IndexBackend.h"
+#include "BaseIndexConnectionsPool.h"
 
 #include <MultiThreading/SharedMessageQueue.h>
 
@@ -33,55 +34,34 @@
 
 namespace OrthancDatabases
 {
-  class IndexConnectionsPool : public boost::noncopyable
+  class IndexConnectionsPool : public BaseIndexConnectionsPool
   {
   private:
     class ManagerReference;
 
-    std::unique_ptr<IndexBackend>  backend_;
-    OrthancPluginContext*          context_;
     boost::shared_mutex            connectionsMutex_;
     size_t                         countConnections_;
     std::list<DatabaseManager*>    connections_;
     Orthanc::SharedMessageQueue    availableConnections_;
-    bool                           housekeepingContinue_;
-    boost::thread                  housekeepingThread_;
-    boost::posix_time::time_duration  housekeepingDelay_;
+
+  protected:
 
-    static void HousekeepingThread(IndexConnectionsPool* that);
+    virtual DatabaseManager* GetConnection() ORTHANC_OVERRIDE;
+    
+    virtual void ReleaseConnection(DatabaseManager* manager) ORTHANC_OVERRIDE;
+
+    virtual void PerformPoolHousekeeping() ORTHANC_OVERRIDE;
 
   public:
     IndexConnectionsPool(IndexBackend* backend /* takes ownership */,
                          size_t countConnections,
                          unsigned int houseKeepingDelaySeconds);
 
-    ~IndexConnectionsPool();
-
-    OrthancPluginContext* GetContext() const
-    {
-      return context_;
-    }
-
-    void OpenConnections(bool hasIdentifierTags,
-                         const std::list<IdentifierTag>& identifierTags);
-
-    void CloseConnections();
+    virtual ~IndexConnectionsPool();
 
-    class Accessor : public boost::noncopyable
-    {
-    private:
-      boost::shared_lock<boost::shared_mutex>  lock_;
-      IndexConnectionsPool&                    pool_;
-      DatabaseManager*                         manager_;
-      
-    public:
-      explicit Accessor(IndexConnectionsPool& pool);
+    virtual void OpenConnections(bool hasIdentifierTags,
+                                 const std::list<IdentifierTag>& identifierTags) ORTHANC_OVERRIDE;
 
-      ~Accessor();
-
-      IndexBackend& GetBackend() const;
-
-      DatabaseManager& GetManager() const;
-    };
+    virtual void CloseConnections() ORTHANC_OVERRIDE;
   };
 }
--- a/Framework/Plugins/IndexUnitTests.h	Tue Jun 17 12:41:37 2025 +0200
+++ b/Framework/Plugins/IndexUnitTests.h	Fri Jun 27 15:29:59 2025 +0200
@@ -40,12 +40,17 @@
 
 #if ORTHANC_ENABLE_POSTGRESQL == 1
 #  define HAS_REVISIONS 1
+// we can not test patient protection in PG because it is now deeply intricated in the CreateInstance function that is too difficult to call from here
+#  define CAN_TEST_PATIENT_PROTECTION 0
 #elif ORTHANC_ENABLE_MYSQL == 1
 #  define HAS_REVISIONS 0
+#  define CAN_TEST_PATIENT_PROTECTION 1
 #elif ORTHANC_ENABLE_SQLITE == 1
 #  define HAS_REVISIONS 1
+#  define CAN_TEST_PATIENT_PROTECTION 1
 #elif ORTHANC_ENABLE_ODBC == 1
 #  define HAS_REVISIONS 1
+#  define CAN_TEST_PATIENT_PROTECTION 1
 #else
 #  error Unknown database backend
 #endif
@@ -369,54 +374,53 @@
   ASSERT_EQ(0u, db.GetResourcesCount(*manager, OrthancPluginResourceType_Study));
   ASSERT_EQ(0u, db.GetResourcesCount(*manager, OrthancPluginResourceType_Series));
 
-  int64_t a = db.CreateResource(*manager, "study", OrthancPluginResourceType_Study);
-  ASSERT_TRUE(db.IsExistingResource(*manager, a));
-  ASSERT_FALSE(db.IsExistingResource(*manager, a + 1));
+  int64_t studyId = db.CreateResource(*manager, "study", OrthancPluginResourceType_Study);
+  ASSERT_TRUE(db.IsExistingResource(*manager, studyId));
+  ASSERT_FALSE(db.IsExistingResource(*manager, studyId + 1));
 
-  int64_t b;
+  int64_t tmp;
   OrthancPluginResourceType t;
-  ASSERT_FALSE(db.LookupResource(b, t, *manager, "world"));
-  ASSERT_TRUE(db.LookupResource(b, t, *manager, "study"));
-  ASSERT_EQ(a, b);
+  ASSERT_FALSE(db.LookupResource(tmp, t, *manager, "world"));
+  ASSERT_TRUE(db.LookupResource(tmp, t, *manager, "study"));
+  ASSERT_EQ(studyId, tmp);
   ASSERT_EQ(OrthancPluginResourceType_Study, t);
   
-  b = db.CreateResource(*manager, "series", OrthancPluginResourceType_Series);
-  ASSERT_NE(a, b);
+  int64_t seriesId = db.CreateResource(*manager, "series", OrthancPluginResourceType_Series);
+  ASSERT_NE(studyId, seriesId);
 
-  ASSERT_EQ("study", db.GetPublicId(*manager, a));
-  ASSERT_EQ("series", db.GetPublicId(*manager, b));
-  ASSERT_EQ(OrthancPluginResourceType_Study, db.GetResourceType(*manager, a));
-  ASSERT_EQ(OrthancPluginResourceType_Series, db.GetResourceType(*manager, b));
+  ASSERT_EQ("study", db.GetPublicId(*manager, studyId));
+  ASSERT_EQ("series", db.GetPublicId(*manager, seriesId));
+  ASSERT_EQ(OrthancPluginResourceType_Study, db.GetResourceType(*manager, studyId));
+  ASSERT_EQ(OrthancPluginResourceType_Series, db.GetResourceType(*manager, seriesId));
 
-  db.AttachChild(*manager, a, b);
+  db.AttachChild(*manager, studyId, seriesId);
 
-  int64_t c;
-  ASSERT_FALSE(db.LookupParent(c, *manager, a));
-  ASSERT_TRUE(db.LookupParent(c, *manager, b));
-  ASSERT_EQ(a, c);
+  ASSERT_FALSE(db.LookupParent(tmp, *manager, studyId));
+  ASSERT_TRUE(db.LookupParent(tmp, *manager, seriesId));
+  ASSERT_EQ(studyId, tmp);
 
-  c = db.CreateResource(*manager, "series2", OrthancPluginResourceType_Series);
-  db.AttachChild(*manager, a, c);
+  int64_t series2Id = db.CreateResource(*manager, "series2", OrthancPluginResourceType_Series);
+  db.AttachChild(*manager, studyId, series2Id);
 
   ASSERT_EQ(3u, db.GetAllResourcesCount(*manager));
   ASSERT_EQ(0u, db.GetResourcesCount(*manager, OrthancPluginResourceType_Patient));
   ASSERT_EQ(1u, db.GetResourcesCount(*manager, OrthancPluginResourceType_Study));
   ASSERT_EQ(2u, db.GetResourcesCount(*manager, OrthancPluginResourceType_Series));
 
-  ASSERT_FALSE(db.GetParentPublicId(s, *manager, a));
-  ASSERT_TRUE(db.GetParentPublicId(s, *manager, b));  ASSERT_EQ("study", s);
-  ASSERT_TRUE(db.GetParentPublicId(s, *manager, c));  ASSERT_EQ("study", s);
+  ASSERT_FALSE(db.GetParentPublicId(s, *manager, studyId));
+  ASSERT_TRUE(db.GetParentPublicId(s, *manager, seriesId));  ASSERT_EQ("study", s);
+  ASSERT_TRUE(db.GetParentPublicId(s, *manager, series2Id));  ASSERT_EQ("study", s);
 
   std::list<std::string> children;
-  db.GetChildren(children, *manager, a);
+  db.GetChildren(children, *manager, studyId);
   ASSERT_EQ(2u, children.size());
-  db.GetChildren(children, *manager, b);
+  db.GetChildren(children, *manager, seriesId);
   ASSERT_EQ(0u, children.size());
-  db.GetChildren(children, *manager, c);
+  db.GetChildren(children, *manager, series2Id);
   ASSERT_EQ(0u, children.size());
 
   std::list<std::string> cp;
-  db.GetChildrenPublicId(cp, *manager, a);
+  db.GetChildrenPublicId(cp, *manager, studyId);
   ASSERT_EQ(2u, cp.size());
   ASSERT_TRUE(cp.front() == "series" || cp.front() == "series2");
   ASSERT_TRUE(cp.back() == "series" || cp.back() == "series2");
@@ -435,17 +439,17 @@
   ASSERT_NE(pub.front(), pub.back());
 
   std::list<int64_t> ci;
-  db.GetChildrenInternalId(ci, *manager, a);
+  db.GetChildrenInternalId(ci, *manager, studyId);
   ASSERT_EQ(2u, ci.size());
-  ASSERT_TRUE(ci.front() == b || ci.front() == c);
-  ASSERT_TRUE(ci.back() == b || ci.back() == c);
+  ASSERT_TRUE(ci.front() == seriesId || ci.front() == series2Id);
+  ASSERT_TRUE(ci.back() == seriesId || ci.back() == series2Id);
   ASSERT_NE(ci.front(), ci.back());
 
-  db.SetMetadata(*manager, a, Orthanc::MetadataType_ModifiedFrom, "modified", 42);
-  db.SetMetadata(*manager, a, Orthanc::MetadataType_LastUpdate, "update2", 43);
+  db.SetMetadata(*manager, studyId, Orthanc::MetadataType_ModifiedFrom, "modified", 42);
+  db.SetMetadata(*manager, studyId, Orthanc::MetadataType_LastUpdate, "update2", 43);
   int64_t revision = -1;
-  ASSERT_FALSE(db.LookupMetadata(s, revision, *manager, b, Orthanc::MetadataType_LastUpdate));
-  ASSERT_TRUE(db.LookupMetadata(s, revision, *manager, a, Orthanc::MetadataType_LastUpdate));
+  ASSERT_FALSE(db.LookupMetadata(s, revision, *manager, seriesId, Orthanc::MetadataType_LastUpdate));
+  ASSERT_TRUE(db.LookupMetadata(s, revision, *manager, studyId, Orthanc::MetadataType_LastUpdate));
   ASSERT_EQ("update2", s);
 
 #if HAS_REVISIONS == 1
@@ -454,8 +458,8 @@
   ASSERT_EQ(0, revision);
 #endif
 
-  db.SetMetadata(*manager, a, Orthanc::MetadataType_LastUpdate, reinterpret_cast<const char*>(UTF8), 44);
-  ASSERT_TRUE(db.LookupMetadata(s, revision, *manager, a, Orthanc::MetadataType_LastUpdate));
+  db.SetMetadata(*manager, studyId, Orthanc::MetadataType_LastUpdate, reinterpret_cast<const char*>(UTF8), 44);
+  ASSERT_TRUE(db.LookupMetadata(s, revision, *manager, studyId, Orthanc::MetadataType_LastUpdate));
   ASSERT_STREQ(reinterpret_cast<const char*>(UTF8), s.c_str());
 
 #if HAS_REVISIONS == 1
@@ -465,12 +469,12 @@
 #endif
 
   std::list<int32_t> md;
-  db.ListAvailableMetadata(md, *manager, a);
+  db.ListAvailableMetadata(md, *manager, studyId);
   ASSERT_EQ(2u, md.size());
   ASSERT_TRUE(md.front() == Orthanc::MetadataType_ModifiedFrom || md.back() == Orthanc::MetadataType_ModifiedFrom);
   ASSERT_TRUE(md.front() == Orthanc::MetadataType_LastUpdate || md.back() == Orthanc::MetadataType_LastUpdate);
   std::string mdd;
-  ASSERT_TRUE(db.LookupMetadata(mdd, revision, *manager, a, Orthanc::MetadataType_ModifiedFrom));
+  ASSERT_TRUE(db.LookupMetadata(mdd, revision, *manager, studyId, Orthanc::MetadataType_ModifiedFrom));
   ASSERT_EQ("modified", mdd);
 
 #if HAS_REVISIONS == 1
@@ -479,7 +483,7 @@
   ASSERT_EQ(0, revision);
 #endif
 
-  ASSERT_TRUE(db.LookupMetadata(mdd, revision, *manager, a, Orthanc::MetadataType_LastUpdate));
+  ASSERT_TRUE(db.LookupMetadata(mdd, revision, *manager, studyId, Orthanc::MetadataType_LastUpdate));
   ASSERT_EQ(reinterpret_cast<const char*>(UTF8), mdd);
 
 #if HAS_REVISIONS == 1
@@ -488,16 +492,16 @@
   ASSERT_EQ(0, revision);
 #endif
 
-  db.ListAvailableMetadata(md, *manager, b);
+  db.ListAvailableMetadata(md, *manager, seriesId);
   ASSERT_EQ(0u, md.size());
 
-  ASSERT_TRUE(db.LookupMetadata(s, revision, *manager, a, Orthanc::MetadataType_LastUpdate));
-  db.DeleteMetadata(*manager, a, Orthanc::MetadataType_LastUpdate);
-  ASSERT_FALSE(db.LookupMetadata(s, revision, *manager, a, Orthanc::MetadataType_LastUpdate));
-  db.DeleteMetadata(*manager, b, Orthanc::MetadataType_LastUpdate);
-  ASSERT_FALSE(db.LookupMetadata(s, revision, *manager, a, Orthanc::MetadataType_LastUpdate));
+  ASSERT_TRUE(db.LookupMetadata(s, revision, *manager, studyId, Orthanc::MetadataType_LastUpdate));
+  db.DeleteMetadata(*manager, studyId, Orthanc::MetadataType_LastUpdate);
+  ASSERT_FALSE(db.LookupMetadata(s, revision, *manager, studyId, Orthanc::MetadataType_LastUpdate));
+  db.DeleteMetadata(*manager, seriesId, Orthanc::MetadataType_LastUpdate);
+  ASSERT_FALSE(db.LookupMetadata(s, revision, *manager, studyId, Orthanc::MetadataType_LastUpdate));
 
-  db.ListAvailableMetadata(md, *manager, a);
+  db.ListAvailableMetadata(md, *manager, studyId);
   ASSERT_EQ(1u, md.size());
   ASSERT_EQ(Orthanc::MetadataType_ModifiedFrom, md.front());
 
@@ -507,37 +511,38 @@
 
   std::list<int32_t> fc;
 
-  OrthancPluginAttachment a1;
-  a1.uuid = "uuid1";
-  a1.contentType = Orthanc::FileContentType_Dicom;
-  a1.uncompressedSize = 42;
-  a1.uncompressedHash = "md5_1";
-  a1.compressionType = Orthanc::CompressionType_None;
-  a1.compressedSize = 42;
-  a1.compressedHash = "md5_1";
+  OrthancPluginAttachment att1;
+  att1.uuid = "uuid1";
+  att1.contentType = Orthanc::FileContentType_Dicom;
+  att1.uncompressedSize = 42;
+  att1.uncompressedHash = "md5_1";
+  att1.compressionType = Orthanc::CompressionType_None;
+  att1.compressedSize = 42;
+  att1.compressedHash = "md5_1";
     
-  OrthancPluginAttachment a2;
-  a2.uuid = "uuid2";
-  a2.contentType = Orthanc::FileContentType_DicomAsJson;
-  a2.uncompressedSize = 4242;
-  a2.uncompressedHash = "md5_2";
-  a2.compressionType = Orthanc::CompressionType_None;
-  a2.compressedSize = 4242;
-  a2.compressedHash = "md5_2";
+  OrthancPluginAttachment att2;
+  att2.uuid = "uuid2";
+  att2.contentType = Orthanc::FileContentType_DicomAsJson;
+  att2.uncompressedSize = 4242;
+  att2.uncompressedHash = "md5_2";
+  att2.compressionType = Orthanc::CompressionType_None;
+  att2.compressedSize = 4242;
+  att2.compressedHash = "md5_2";
     
 #if ORTHANC_PLUGINS_HAS_ATTACHMENTS_CUSTOM_DATA == 1
-  db.AddAttachment(*manager, a, a1, 42, "my_custom_data");
+  db.AddAttachment(*manager, studyId, att1, 42, "my_custom_data");
+  db.ListAvailableAttachments(fc, *manager, studyId);
 #else
-  db.AddAttachment(*manager, a, a1, 42);
+  db.AddAttachment(*manager, studyId, att1, 42);
 #endif
 
-  db.ListAvailableAttachments(fc, *manager, a);
+  db.ListAvailableAttachments(fc, *manager, studyId);
   ASSERT_EQ(1u, fc.size());
   ASSERT_EQ(Orthanc::FileContentType_Dicom, fc.front());
-  db.AddAttachment(*manager, a, a2, 43);
-  db.ListAvailableAttachments(fc, *manager, a);
+  db.AddAttachment(*manager, studyId, att2, 43);
+  db.ListAvailableAttachments(fc, *manager, studyId);
   ASSERT_EQ(2u, fc.size());
-  ASSERT_FALSE(db.LookupAttachment(*output, revision, *manager, b, Orthanc::FileContentType_Dicom));
+  ASSERT_FALSE(db.LookupAttachment(*output, revision, *manager, seriesId, Orthanc::FileContentType_Dicom));
 
 #if ORTHANC_PLUGINS_HAS_ATTACHMENTS_CUSTOM_DATA == 1
   {
@@ -576,7 +581,7 @@
   expectedAttachment->compressionType = Orthanc::CompressionType_None;
   expectedAttachment->compressedSize = 42;
   expectedAttachment->compressedHash = "md5_1";
-  ASSERT_TRUE(db.LookupAttachment(*output, revision, *manager, a, Orthanc::FileContentType_Dicom));
+  ASSERT_TRUE(db.LookupAttachment(*output, revision, *manager, studyId, Orthanc::FileContentType_Dicom));
 
 #if HAS_REVISIONS == 1
   ASSERT_EQ(42, revision);
@@ -593,7 +598,7 @@
   expectedAttachment->compressedSize = 4242;
   expectedAttachment->compressedHash = "md5_2";
   revision = -1;
-  ASSERT_TRUE(db.LookupAttachment(*output, revision, *manager, a, Orthanc::FileContentType_DicomAsJson));
+  ASSERT_TRUE(db.LookupAttachment(*output, revision, *manager, studyId, Orthanc::FileContentType_DicomAsJson));
 
 #if HAS_REVISIONS == 1
   ASSERT_EQ(43, revision);
@@ -601,21 +606,21 @@
   ASSERT_EQ(0, revision);
 #endif
 
-  db.ListAvailableAttachments(fc, *manager, b);
+  db.ListAvailableAttachments(fc, *manager, seriesId);
   ASSERT_EQ(0u, fc.size());
-  db.DeleteAttachment(*output, *manager, a, Orthanc::FileContentType_Dicom);
-  db.ListAvailableAttachments(fc, *manager, a);
+  db.DeleteAttachment(*output, *manager, studyId, Orthanc::FileContentType_Dicom);
+  db.ListAvailableAttachments(fc, *manager, studyId);
   ASSERT_EQ(1u, fc.size());
   ASSERT_EQ(Orthanc::FileContentType_DicomAsJson, fc.front());
-  db.DeleteAttachment(*output, *manager, a, Orthanc::FileContentType_DicomAsJson);
-  db.ListAvailableAttachments(fc, *manager, a);
+  db.DeleteAttachment(*output, *manager, studyId, Orthanc::FileContentType_DicomAsJson);
+  db.ListAvailableAttachments(fc, *manager, studyId);
   ASSERT_EQ(0u, fc.size());
 
-  db.SetIdentifierTag(*manager, a, 0x0010, 0x0020, "patient");
-  db.SetIdentifierTag(*manager, a, 0x0020, 0x000d, "study");
-  db.SetMainDicomTag(*manager, a, 0x0010, 0x0020, "patient");
-  db.SetMainDicomTag(*manager, a, 0x0020, 0x000d, "study");
-  db.SetMainDicomTag(*manager, a, 0x0008, 0x1030, reinterpret_cast<const char*>(UTF8));
+  db.SetIdentifierTag(*manager, studyId, 0x0010, 0x0020, "patient");
+  db.SetIdentifierTag(*manager, studyId, 0x0020, 0x000d, "study");
+  db.SetMainDicomTag(*manager, studyId, 0x0010, 0x0020, "patient");
+  db.SetMainDicomTag(*manager, studyId, 0x0020, 0x000d, "study");
+  db.SetMainDicomTag(*manager, studyId, 0x0008, 0x1030, reinterpret_cast<const char*>(UTF8));
 
   expectedDicomTags.clear();
   expectedDicomTags.push_back(OrthancPluginDicomTag());
@@ -632,13 +637,13 @@
   expectedDicomTags.back().value = reinterpret_cast<const char*>(UTF8);
 
   countDicomTags = 0;
-  db.GetMainDicomTags(*output, *manager, a);
+  db.GetMainDicomTags(*output, *manager, studyId);
   ASSERT_EQ(3u, countDicomTags);
 
   db.LookupIdentifier(ci, *manager, OrthancPluginResourceType_Study, 0x0010, 0x0020, 
                       OrthancPluginIdentifierConstraint_Equal, "patient");
   ASSERT_EQ(1u, ci.size());
-  ASSERT_EQ(a, ci.front());
+  ASSERT_EQ(studyId, ci.front());
   db.LookupIdentifier(ci, *manager, OrthancPluginResourceType_Study, 0x0010, 0x0020, 
                       OrthancPluginIdentifierConstraint_Equal, "study");
   ASSERT_EQ(0u, ci.size());
@@ -668,8 +673,11 @@
   db.GetAllPublicIds(pub, *manager, OrthancPluginResourceType_Instance); ASSERT_EQ(0u, pub.size());
   ASSERT_EQ(3u, db.GetAllResourcesCount(*manager));
 
-  ASSERT_EQ(0u, db.GetUnprotectedPatientsCount(*manager));  // No patient was inserted
-  ASSERT_TRUE(db.IsExistingResource(*manager, c));
+  #if CAN_TEST_PATIENT_PROTECTION == 1
+    ASSERT_EQ(0u, db.GetUnprotectedPatientsCount(*manager));  // No patient was inserted
+  #endif
+
+  ASSERT_TRUE(db.IsExistingResource(*manager, series2Id));
 
   {
     // A transaction is needed here for MySQL, as it was not possible
@@ -681,7 +689,7 @@
     deletedResources.clear();
     remainingAncestor.reset();
     
-    db.DeleteResource(*output, *manager, c);
+    db.DeleteResource(*output, *manager, series2Id);
 
     ASSERT_EQ(0u, deletedAttachments.size());
     ASSERT_EQ(1u, deletedResources.size());
@@ -697,22 +705,22 @@
   deletedResources.clear();
   remainingAncestor.reset();
 
-  ASSERT_FALSE(db.IsExistingResource(*manager, c));
-  ASSERT_TRUE(db.IsExistingResource(*manager, a));
-  ASSERT_TRUE(db.IsExistingResource(*manager, b));
+  ASSERT_FALSE(db.IsExistingResource(*manager, series2Id));
+  ASSERT_TRUE(db.IsExistingResource(*manager, studyId));
+  ASSERT_TRUE(db.IsExistingResource(*manager, seriesId));
   ASSERT_EQ(2u, db.GetAllResourcesCount(*manager));
 
   {
     // An explicit transaction is needed here
     manager->StartTransaction(TransactionType_ReadWrite);
-    db.DeleteResource(*output, *manager, a);
+    db.DeleteResource(*output, *manager, studyId);  // delete the study that only has one series left -> 2 resources shall be deleted
     manager->CommitTransaction();
   }
 
   ASSERT_EQ(0u, db.GetAllResourcesCount(*manager));
-  ASSERT_FALSE(db.IsExistingResource(*manager, a));
-  ASSERT_FALSE(db.IsExistingResource(*manager, b));
-  ASSERT_FALSE(db.IsExistingResource(*manager, c));
+  ASSERT_FALSE(db.IsExistingResource(*manager, studyId));
+  ASSERT_FALSE(db.IsExistingResource(*manager, seriesId));
+  ASSERT_FALSE(db.IsExistingResource(*manager, series2Id));
 
   ASSERT_EQ(0u, deletedAttachments.size());
   ASSERT_EQ(2u, deletedResources.size());
@@ -721,6 +729,7 @@
   ASSERT_FALSE(remainingAncestor.get() != NULL);
   
   ASSERT_EQ(0u, db.GetAllResourcesCount(*manager));
+#if CAN_TEST_PATIENT_PROTECTION == 1
   ASSERT_EQ(0u, db.GetUnprotectedPatientsCount(*manager));
   int64_t p1 = db.CreateResource(*manager, "patient1", OrthancPluginResourceType_Patient);
   int64_t p2 = db.CreateResource(*manager, "patient2", OrthancPluginResourceType_Patient);
@@ -752,6 +761,14 @@
   ASSERT_EQ(p1, r);
 
   {
+    manager->StartTransaction(TransactionType_ReadWrite);
+    db.DeleteResource(*output, *manager, p1);
+    db.DeleteResource(*output, *manager, p3);
+    manager->CommitTransaction();
+  }
+#endif
+
+  {
     // Test creating a large property of 16MB (large properties are
     // notably necessary to serialize jobs)
     // https://groups.google.com/g/orthanc-users/c/1Y3nTBdr0uE/m/K7PA5pboAgAJ
@@ -777,14 +794,6 @@
     ASSERT_EQ(longProperty, tmp);
   }
 
-  {
-    manager->StartTransaction(TransactionType_ReadWrite);
-    db.DeleteResource(*output, *manager, p1);
-    db.DeleteResource(*output, *manager, p3);
-    manager->CommitTransaction();
-  }
-
-
   for (size_t level = 0; level < 4; level++)
   {
     for (size_t attachmentLevel = 0; attachmentLevel < 4; attachmentLevel++)
--- a/Framework/PostgreSQL/PostgreSQLDatabase.cpp	Tue Jun 17 12:41:37 2025 +0200
+++ b/Framework/PostgreSQL/PostgreSQLDatabase.cpp	Fri Jun 27 15:29:59 2025 +0200
@@ -63,7 +63,7 @@
   {
     if (pg_ != NULL)
     {
-      LOG(INFO) << "Closing connection to PostgreSQL";
+      LOG(TRACE) << "Closing connection to PostgreSQL";
       PQfinish(reinterpret_cast<PGconn*>(pg_));
       pg_ = NULL;
     }
--- a/PostgreSQL/CMakeLists.txt	Tue Jun 17 12:41:37 2025 +0200
+++ b/PostgreSQL/CMakeLists.txt	Fri Jun 27 15:29:59 2025 +0200
@@ -95,6 +95,7 @@
   POSTGRESQL_UPGRADE_REV2_TO_REV3    ${CMAKE_SOURCE_DIR}/Plugins/SQL/Upgrades/Rev2ToRev3.sql
   POSTGRESQL_UPGRADE_REV3_TO_REV4    ${CMAKE_SOURCE_DIR}/Plugins/SQL/Upgrades/Rev3ToRev4.sql
   POSTGRESQL_UPGRADE_REV4_TO_REV5    ${CMAKE_SOURCE_DIR}/Plugins/SQL/Upgrades/Rev4ToRev5.sql
+  POSTGRESQL_UPGRADE_REV5_TO_REV599  ${CMAKE_SOURCE_DIR}/Plugins/SQL/Upgrades/Rev5ToRev599.sql
   )
 
 
--- a/PostgreSQL/NEWS	Tue Jun 17 12:41:37 2025 +0200
+++ b/PostgreSQL/NEWS	Fri Jun 27 15:29:59 2025 +0200
@@ -1,6 +1,31 @@
 Pending changes in the mainline
 ===============================
 
+DB schema revision: 5
+Minimum plugin SDK (for build): 1.12.5
+Optimal plugin SDK (for build): 1.12.5
+Minimum Orthanc runtime: 1.12.5
+Optimal Orthanc runtime: 1.12.6
+
+Minimal Postgresql Server version: 9
+Optimal Postgresql Server version: 11+
+
+* New configuration "UseDynamicConnectionPool" to dynamically create/release
+  connections to the PostgreSQL database.  
+  When this option is disabled (default), the connections are created once
+  at the start of the plugin and are kept alive for the whole execution.
+  Using the dynamic mode enables cleanup of some temporary tables that
+  could otherwise accumulate dead rows.
+  When using a dynamic pool, connections are released either after 60 seconds
+  of being idle or one hour after their creation.
+* New metrics "orthanc_index_active_connections_count" showing the current number
+  of active connections.
+
+Maintenance:
+* Optimized the CreateInstance SQL query.
+* Internals: 
+  - The PatientRecyclingOrder has been refactored and is now stored in
+    Patient metadata (18: IsProtected, 19: PatientRecyclingOrder)
 
 
 
--- a/PostgreSQL/Plugins/IndexPlugin.cpp	Tue Jun 17 12:41:37 2025 +0200
+++ b/PostgreSQL/Plugins/IndexPlugin.cpp	Fri Jun 27 15:29:59 2025 +0200
@@ -80,11 +80,12 @@
     try
     {
       const size_t countConnections = postgresql.GetUnsignedIntegerValue("IndexConnectionsCount", 50);
+      const bool useDynamicConnectionPool = postgresql.GetBooleanValue("UseDynamicConnectionPool", false);
       const unsigned int housekeepingDelaySeconds = postgresql.GetUnsignedIntegerValue("HousekeepingInterval", 1);
 
       OrthancDatabases::PostgreSQLParameters parameters(postgresql);
       OrthancDatabases::IndexBackend::Register(
-        new OrthancDatabases::PostgreSQLIndex(context, parameters, readOnly), countConnections,
+        new OrthancDatabases::PostgreSQLIndex(context, parameters, readOnly), countConnections, useDynamicConnectionPool,
         parameters.GetMaxConnectionRetries(), housekeepingDelaySeconds);
     }
     catch (Orthanc::OrthancException& e)
--- a/PostgreSQL/Plugins/PostgreSQLIndex.cpp	Tue Jun 17 12:41:37 2025 +0200
+++ b/PostgreSQL/Plugins/PostgreSQLIndex.cpp	Fri Jun 27 15:29:59 2025 +0200
@@ -49,7 +49,7 @@
   static const GlobalProperty GlobalProperty_HasComputeStatisticsReadOnly = GlobalProperty_DatabaseInternal4;
 }
 
-#define CURRENT_DB_REVISION 5
+#define CURRENT_DB_REVISION 599
 
 namespace OrthancDatabases
 {
@@ -248,6 +248,19 @@
             currentRevision = 5;
           }
 
+          if (currentRevision == 5)
+          {
+            LOG(WARNING) << "Upgrading DB schema from revision 5 to revision 599";
+
+            std::string query;
+
+            Orthanc::EmbeddedResources::GetFileResource
+              (query, Orthanc::EmbeddedResources::POSTGRESQL_UPGRADE_REV5_TO_REV599);
+            t.GetDatabaseTransaction().ExecuteMultiLines(query);
+            hasAppliedAnUpgrade = true;
+            currentRevision = 599;
+          }
+
           if (hasAppliedAnUpgrade)
           {
             LOG(WARNING) << "Upgrading DB schema by applying PrepareIndex.sql";
@@ -457,6 +470,30 @@
     uncompressedSize = statement.ReadInteger64(5);
   }
 
+  void PostgreSQLIndex::DeleteAttachment(IDatabaseBackendOutput& output,
+                                         DatabaseManager& manager,
+                                         int64_t id,
+                                         int32_t attachment)
+  {
+    {
+      DatabaseManager::CachedStatement statement(
+        STATEMENT_FROM_HERE, manager,
+        "SELECT DeleteAttachment(${id}, ${type})");
+
+      statement.SetParameterType("id", ValueType_Integer64);
+      statement.SetParameterType("type", ValueType_Integer32);
+
+      Dictionary args;
+      args.SetIntegerValue("id", id);
+      args.SetInteger32Value("type", attachment);
+    
+      statement.ExecuteWithoutResult(args);
+    }
+
+    SignalDeletedFiles(output, manager);
+  }
+
+
   void PostgreSQLIndex::ClearDeletedFiles(DatabaseManager& manager)
   {
     { // note: the temporary table lifespan is the session, not the transaction -> that's why we need the IF NOT EXISTS
@@ -466,7 +503,6 @@
         );
       statement.ExecuteWithoutResult();
     }
-
   }
 
   void PostgreSQLIndex::ClearDeletedResources(DatabaseManager& manager)
@@ -489,11 +525,11 @@
 
       statement.Execute();
     }
-
   }
 
   void PostgreSQLIndex::ClearRemainingAncestor(DatabaseManager& manager)
   {
+    // not used anymore in PostgreSQL
   }
 
   void PostgreSQLIndex::DeleteResource(IDatabaseBackendOutput& output,
@@ -783,6 +819,111 @@
     throw Orthanc::OrthancException(Orthanc::ErrorCode_Database);
   }
 
+  void PostgreSQLIndex::SetProtectedPatient(DatabaseManager& manager,
+                                            int64_t internalId, 
+                                            bool isProtected)
+  {
+    std::unique_ptr<DatabaseManager::CachedStatement> statement;
+    Dictionary args;
+
+    if (isProtected)
+    {
+      statement.reset(new DatabaseManager::CachedStatement(
+                        STATEMENT_FROM_HERE, manager,
+                        "SELECT ProtectPatient(${id})"));
+    }
+    else
+    {
+      statement.reset(new DatabaseManager::CachedStatement(
+                        STATEMENT_FROM_HERE, manager,
+                        "SELECT UnprotectPatient(${id})"));
+    }        
+
+    statement->SetParameterType("id", ValueType_Integer64);
+    args.SetIntegerValue("id", internalId);
+      
+    statement->Execute(args);
+  }
+
+  bool PostgreSQLIndex::IsProtectedPatient(DatabaseManager& manager,
+                                        int64_t internalId)
+  {
+    std::string value;
+    int64_t revision;
+    
+    if (LookupMetadata(value, revision, manager, internalId, 18)) // 18 = IsProtected
+    {
+      return value == "true";
+    }
+
+    return false;
+  }
+
+  bool PostgreSQLIndex::SelectPatientToRecycle(int64_t& internalId /*out*/,
+                                               DatabaseManager& manager)
+  {
+    DatabaseManager::CachedStatement statement(
+      STATEMENT_FROM_HERE, manager,
+      "SELECT r.internalId "
+      "FROM Resources r "
+      "JOIN Metadata m ON r.internalId = m.id AND m.type = 19 " // 19 = PatientRecyclingOrder
+      "WHERE r.resourceType = 0 "
+      "  AND NOT EXISTS "
+      "   (SELECT 1 FROM Metadata m "
+      "    WHERE m.id = r.internalId AND m.type = 18 AND m.value = 'true') "  // 18 = IsProtected
+      " ORDER BY CAST(m.value AS INTEGER) ASC LIMIT 1;");
+    
+    statement.SetReadOnly(true);
+    statement.Execute();
+
+    if (statement.IsDone())
+    {
+      return false;
+    }
+    else
+    {
+      internalId = statement.ReadInteger64(0);
+      return true;
+    }
+  }
+
+    
+  bool PostgreSQLIndex::SelectPatientToRecycle(int64_t& internalId /*out*/,
+                                               DatabaseManager& manager,
+                                               int64_t patientIdToAvoid)
+  {
+    DatabaseManager::CachedStatement statement(
+      STATEMENT_FROM_HERE, manager,
+      "SELECT r.internalId "
+      "FROM Resources r "
+      "JOIN Metadata m ON r.internalId = m.id AND m.type = 19 " // 19 = PatientRecyclingOrder
+      "WHERE r.resourceType = 0 "
+      "  AND r.internalId != ${id} "
+      "  AND NOT EXISTS "
+      "   (SELECT 1 FROM Metadata m "
+      "    WHERE m.id = r.internalId AND m.type = 18 AND m.value = 'true') "  // 18 = IsProtected
+      " ORDER BY CAST(m.value AS INTEGER) ASC LIMIT 1;");
+
+    statement.SetReadOnly(true);
+    statement.SetParameterType("id", ValueType_Integer64);
+
+    Dictionary args;
+    args.SetIntegerValue("id", patientIdToAvoid);
+
+    statement.Execute(args);
+
+    if (statement.IsDone())
+    {
+      return false;
+    }
+    else
+    {
+      internalId = statement.ReadInteger64(0);
+      return true;
+    }
+  }
+
+
   bool PostgreSQLIndex::HasPerformDbHousekeeping()
   {
     return !IsReadOnly(); // Don't start HK on ReadOnly databases !
--- a/PostgreSQL/Plugins/PostgreSQLIndex.h	Tue Jun 17 12:41:37 2025 +0200
+++ b/PostgreSQL/Plugins/PostgreSQLIndex.h	Fri Jun 27 15:29:59 2025 +0200
@@ -95,6 +95,11 @@
                                 DatabaseManager& manager,
                                 int64_t id) ORTHANC_OVERRIDE;
 
+    virtual void DeleteAttachment(IDatabaseBackendOutput& output,
+                                  DatabaseManager& manager,
+                                  int64_t id,
+                                  int32_t attachment) ORTHANC_OVERRIDE;
+
     virtual void SetResourcesContent(DatabaseManager& manager,
                                      uint32_t countIdentifierTags,
                                      const OrthancPluginResourcesContentTags* identifierTags,
@@ -127,9 +132,24 @@
 
     virtual int64_t GetLastChangeIndex(DatabaseManager& manager) ORTHANC_OVERRIDE;
 
+    // This is now obsolete
     virtual void TagMostRecentPatient(DatabaseManager& manager,
                                       int64_t patient) ORTHANC_OVERRIDE;
 
+    virtual void SetProtectedPatient(DatabaseManager& manager,
+                                     int64_t internalId, 
+                                     bool isProtected) ORTHANC_OVERRIDE;
+
+    virtual bool IsProtectedPatient(DatabaseManager& manager,
+                                    int64_t internalId) ORTHANC_OVERRIDE;
+
+    virtual bool SelectPatientToRecycle(int64_t& internalId /*out*/,
+                                        DatabaseManager& manager) ORTHANC_OVERRIDE;
+    
+    virtual bool SelectPatientToRecycle(int64_t& internalId /*out*/,
+                                        DatabaseManager& manager,
+                                        int64_t patientIdToAvoid) ORTHANC_OVERRIDE;
+
     // New primitive since Orthanc 1.12.0
     virtual bool HasLabelsSupport() const ORTHANC_OVERRIDE
     {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/PostgreSQL/Plugins/SQL/Downgrades/Rev599ToRev5.sql	Fri Jun 27 15:29:59 2025 +0200
@@ -0,0 +1,241 @@
+-- This file contains an SQL procedure to downgrade from schema Rev599 to Rev5 (version = 6).
+
+
+-- Re-installs the old PatientRecycling
+-----------
+
+CREATE TABLE IF NOT EXISTS PatientRecyclingOrder(
+       seq BIGSERIAL NOT NULL PRIMARY KEY,
+       patientId BIGINT REFERENCES Resources(internalId) ON DELETE CASCADE,
+       CONSTRAINT UniquePatientId UNIQUE (patientId)
+       );
+
+CREATE INDEX IF NOT EXISTS PatientRecyclingIndex ON PatientRecyclingOrder(patientId);
+
+DROP TRIGGER IF EXISTS PatientAdded ON Resources;
+
+CREATE OR REPLACE FUNCTION PatientAddedOrUpdated(
+    IN patient_id BIGINT,
+    IN is_update BIGINT
+    )
+RETURNS VOID AS $body$
+BEGIN
+    DECLARE
+        newSeq BIGINT;
+    BEGIN
+        IF is_update > 0 THEN
+            -- Note: Protected patients are not listed in this table !  So, they won't be updated
+            WITH deleted_rows AS (
+                DELETE FROM PatientRecyclingOrder
+                WHERE PatientRecyclingOrder.patientId = patient_id
+                RETURNING patientId
+            )
+            INSERT INTO PatientRecyclingOrder (patientId)
+            SELECT patientID FROM deleted_rows
+            WHERE EXISTS(SELECT 1 FROM deleted_rows);
+        ELSE
+            INSERT INTO PatientRecyclingOrder VALUES (DEFAULT, patient_id);
+        END IF;
+    END;
+END;
+$body$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION PatientAddedFunc() 
+RETURNS TRIGGER AS $body$
+BEGIN
+  -- The "0" corresponds to "OrthancPluginResourceType_Patient"
+  IF new.resourceType = 0 THEN
+    PERFORM PatientAddedOrUpdated(new.internalId, 0);
+  END IF;
+  RETURN NULL;
+END;
+$body$ LANGUAGE plpgsql;
+
+DROP TRIGGER IF EXISTS PatientAdded on Resources;
+CREATE TRIGGER PatientAdded
+AFTER INSERT ON Resources
+FOR EACH ROW
+EXECUTE PROCEDURE PatientAddedFunc();
+
+DROP FUNCTION IF EXISTS ProtectPatient(patient_id BIGINT);
+
+DROP FUNCTION IF EXISTS UnprotectPatient;
+
+-- repopulate the PatientRecyclingOrderTable
+WITH UnprotectedPatients AS (SELECT r.internalId
+  FROM Resources r
+  RIGHT JOIN Metadata m ON r.internalId = m.id AND m.type = 19  -- 19 = PatientRecyclingOrder
+  WHERE r.resourceType = 0
+    AND NOT EXISTS (SELECT 1 FROM Metadata m
+                    WHERE m.id = r.internalId AND m.type = 18 AND m.value = 'true') -- 18 = IsProtected
+  ORDER BY CAST(m.value AS INTEGER) ASC)
+
+INSERT INTO PatientRecyclingOrder (patientId)
+SELECT internalId
+FROM UnprotectedPatients;
+
+DROP SEQUENCE IF EXISTS PatientRecyclingOrderSequence;
+
+-- remove the IsProtected and PatientRecyclingOrder metadata
+DELETE FROM Metadata WHERE type IN (18, 19);
+
+-- Re-installs the old CreateInstance method
+-----------
+
+CREATE OR REPLACE FUNCTION CreateInstance(
+  IN patient_public_id TEXT,
+  IN study_public_id TEXT,
+  IN series_public_id TEXT,
+  IN instance_public_id TEXT,
+  OUT is_new_patient BIGINT,
+  OUT is_new_study BIGINT,
+  OUT is_new_series BIGINT,
+  OUT is_new_instance BIGINT,
+  OUT patient_internal_id BIGINT,
+  OUT study_internal_id BIGINT,
+  OUT series_internal_id BIGINT,
+  OUT instance_internal_id BIGINT) AS $body$
+
+BEGIN
+	is_new_patient := 1;
+	is_new_study := 1;
+	is_new_series := 1;
+	is_new_instance := 1;
+
+	BEGIN
+        INSERT INTO "resources" VALUES (DEFAULT, 0, patient_public_id, NULL, 0) RETURNING internalid INTO patient_internal_id;
+    EXCEPTION
+        WHEN unique_violation THEN
+            is_new_patient := 0;
+            SELECT internalid INTO patient_internal_id FROM "resources" WHERE publicId = patient_public_id FOR UPDATE;  -- also locks the resource and its parent to prevent from deletion while we complete this transaction
+    END;
+
+	BEGIN
+        INSERT INTO "resources" VALUES (DEFAULT, 1, study_public_id, patient_internal_id, 0) RETURNING internalid INTO study_internal_id;
+    EXCEPTION
+        WHEN unique_violation THEN
+            is_new_study := 0;
+            SELECT internalid INTO study_internal_id FROM "resources" WHERE publicId = study_public_id FOR UPDATE;  -- also locks the resource and its parent to prevent from deletion while we complete this transaction    END;
+    END;
+
+	BEGIN
+	    INSERT INTO "resources" VALUES (DEFAULT, 2, series_public_id, study_internal_id, 0) RETURNING internalid INTO series_internal_id;
+    EXCEPTION
+        WHEN unique_violation THEN
+            is_new_series := 0;
+            SELECT internalid INTO series_internal_id FROM "resources" WHERE publicId = series_public_id FOR UPDATE;  -- also locks the resource and its parent to prevent from deletion while we complete this transaction    END;
+    END;
+
+  	BEGIN
+		INSERT INTO "resources" VALUES (DEFAULT, 3, instance_public_id, series_internal_id, 0) RETURNING internalid INTO instance_internal_id;
+    EXCEPTION
+        WHEN unique_violation THEN
+            is_new_instance := 0;
+            SELECT internalid INTO instance_internal_id FROM "resources" WHERE publicId = instance_public_id FOR UPDATE;  -- also locks the resource and its parent to prevent from deletion while we complete this transaction
+    END;
+
+    IF is_new_instance > 0 THEN
+        -- Move the patient to the end of the recycling order.
+        PERFORM PatientAddedOrUpdated(patient_internal_id, 1);
+    END IF;  
+END;
+$body$ LANGUAGE plpgsql;
+
+-- Restore the DeleteResource function that has been optimized
+
+------------------- DeleteResource function -------------------
+
+CREATE OR REPLACE FUNCTION DeleteResource(
+    IN id BIGINT,
+    OUT remaining_ancestor_resource_type INTEGER,
+    OUT remaining_anncestor_public_id TEXT) AS $body$
+
+DECLARE
+    deleted_row RECORD;
+    locked_row RECORD;
+
+BEGIN
+
+    SET client_min_messages = warning;   -- suppress NOTICE:  relation "deletedresources" already exists, skipping
+    
+    -- note: temporary tables are created at session (connection) level -> they are likely to exist
+    -- these tables are used by the triggers
+    CREATE TEMPORARY TABLE IF NOT EXISTS DeletedResources(
+        resourceType INTEGER NOT NULL,
+        publicId VARCHAR(64) NOT NULL
+        );
+
+    RESET client_min_messages;
+
+    -- clear the temporary table in case it has been created earlier in the session
+    DELETE FROM DeletedResources;
+    
+    -- create/clear the DeletedFiles temporary table
+    PERFORM CreateDeletedFilesTemporaryTable();
+
+    -- Before deleting an object, we need to lock its parent until the end of the transaction to avoid that
+    -- 2 threads deletes the last 2 instances of a series at the same time -> none of them would realize
+    -- that they are deleting the last instance and the parent resources would not be deleted.
+    -- Locking only the immediate parent is sufficient to prevent from this.
+    SELECT * INTO locked_row FROM resources WHERE internalid = (SELECT parentid FROM resources WHERE internalid = id) FOR UPDATE;
+
+    -- delete the resource itself
+    DELETE FROM Resources WHERE internalId=id RETURNING * INTO deleted_row;
+    -- note: there is a ResourceDeletedFunc trigger that will execute here and delete the parents if there are no remaining children + 
+
+    -- If this resource still has siblings, keep track of the remaining parent
+    -- (a parent that must not be deleted but whose LastUpdate must be updated)
+    SELECT resourceType, publicId INTO remaining_ancestor_resource_type, remaining_anncestor_public_id
+        FROM Resources 
+        WHERE internalId = deleted_row.parentId
+            AND EXISTS (SELECT 1 FROM Resources WHERE parentId = deleted_row.parentId);
+
+END;
+
+$body$ LANGUAGE plpgsql;
+
+
+-- restore the DeletedResource trigger
+
+------------------- ResourceDeleted trigger -------------------
+DROP TRIGGER IF EXISTS ResourceDeleted ON Resources;
+
+-- The following trigger combines 2 triggers from SQLite:
+-- ResourceDeleted + ResourceDeletedParentCleaning
+CREATE OR REPLACE FUNCTION ResourceDeletedFunc() 
+RETURNS TRIGGER AS $body$
+BEGIN
+  -- RAISE NOTICE 'ResourceDeletedFunc %', old.publicId;
+  INSERT INTO DeletedResources VALUES (old.resourceType, old.publicId);
+  
+  -- If this resource is the latest child, delete the parent
+  DELETE FROM Resources WHERE internalId = old.parentId
+                              AND NOT EXISTS (SELECT 1 FROM Resources WHERE parentId = old.parentId);
+  RETURN NULL;
+END;
+$body$ LANGUAGE plpgsql;
+
+DROP TRIGGER IF EXISTS ResourceDeleted on Resources;
+CREATE TRIGGER ResourceDeleted
+AFTER DELETE ON Resources
+FOR EACH ROW
+EXECUTE PROCEDURE ResourceDeletedFunc();
+
+
+-- remove the new DeleteAttachment function
+
+DROP FUNCTION IF EXISTS DeleteAttachment;
+
+-- Restore the ON DELETE CASCADE on the Resources.parentId 
+-- Drop the existing foreign key constraint and add a new one without ON DELETE CASCADE in a single command
+ALTER TABLE Resources
+DROP CONSTRAINT IF EXISTS resources_parentid_fkey,
+ADD CONSTRAINT resources_parentid_fkey FOREIGN KEY (parentId) REFERENCES Resources(internalId) ON DELETE CASCADE;
+
+
+----------
+
+-- set the global properties that actually documents the DB version, revision and some of the capabilities
+-- modify only the ones that have changed
+DELETE FROM GlobalProperties WHERE property IN (4, 11);
+INSERT INTO GlobalProperties VALUES (4, 5); -- GlobalProperty_DatabasePatchLevel
--- a/PostgreSQL/Plugins/SQL/PrepareIndex.sql	Tue Jun 17 12:41:37 2025 +0200
+++ b/PostgreSQL/Plugins/SQL/PrepareIndex.sql	Fri Jun 27 15:29:59 2025 +0200
@@ -14,7 +14,8 @@
        internalId BIGSERIAL NOT NULL PRIMARY KEY,
        resourceType INTEGER NOT NULL,
        publicId VARCHAR(64) NOT NULL,
-       parentId BIGINT REFERENCES Resources(internalId) ON DELETE CASCADE,
+    --    parentId BIGINT REFERENCES Resources(internalId) ON DELETE CASCADE,
+       parentId BIGINT REFERENCES Resources(internalId),
 	   childCount INTEGER,
        CONSTRAINT UniquePublicId UNIQUE (publicId)
        );
@@ -77,12 +78,6 @@
        date VARCHAR(64)
        ); 
 
-CREATE TABLE IF NOT EXISTS PatientRecyclingOrder(
-       seq BIGSERIAL NOT NULL PRIMARY KEY,
-       patientId BIGINT REFERENCES Resources(internalId) ON DELETE CASCADE,
-       CONSTRAINT UniquePatientId UNIQUE (patientId)
-       );
-
 CREATE TABLE IF NOT EXISTS Labels(
         id BIGINT REFERENCES Resources(internalId) ON DELETE CASCADE,
         label TEXT, 
@@ -127,7 +122,6 @@
 
 CREATE INDEX IF NOT EXISTS PublicIndex ON Resources(publicId);
 CREATE INDEX IF NOT EXISTS ResourceTypeIndex ON Resources(resourceType);
-CREATE INDEX IF NOT EXISTS PatientRecyclingIndex ON PatientRecyclingOrder(patientId);
 
 CREATE INDEX IF NOT EXISTS MainDicomTagsIndex ON MainDicomTags(id);
 CREATE INDEX IF NOT EXISTS DicomIdentifiersIndex1 ON DicomIdentifiers(id);
@@ -159,86 +153,24 @@
 END $body$;
 
 
-------------------- PatientAdded trigger & PatientRecyclingOrder -------------------
-DROP TRIGGER IF EXISTS PatientAdded ON Resources;
-
+--------------------- PatientRecyclingOrder -------------------
+-- from rev 99, we always maintain a PatientRecyclingOrder metadata, no matter if the patient is protected or not
 CREATE OR REPLACE FUNCTION PatientAddedOrUpdated(
-    IN patient_id BIGINT,
-    IN is_update BIGINT
+    IN patient_id BIGINT
     )
 RETURNS VOID AS $body$
 BEGIN
     DECLARE
         newSeq BIGINT;
     BEGIN
-        IF is_update > 0 THEN
-            -- Note: Protected patients are not listed in this table !  So, they won't be updated
-            WITH deleted_rows AS (
-                DELETE FROM PatientRecyclingOrder
-                WHERE PatientRecyclingOrder.patientId = patient_id
-                RETURNING patientId
-            )
-            INSERT INTO PatientRecyclingOrder (patientId)
-            SELECT patientID FROM deleted_rows
-            WHERE EXISTS(SELECT 1 FROM deleted_rows);
-        ELSE
-            INSERT INTO PatientRecyclingOrder VALUES (DEFAULT, patient_id);
-        END IF;
+        INSERT INTO Metadata (id, type, value, revision)
+        VALUES (patient_id, 19, nextval('PatientRecyclingOrderSequence')::TEXT, 0)
+        ON CONFLICT (id, type)
+        DO UPDATE SET value = EXCLUDED.value, revision = EXCLUDED.revision;
     END;
 END;
 $body$ LANGUAGE plpgsql;
 
-CREATE OR REPLACE FUNCTION PatientAddedFunc() 
-RETURNS TRIGGER AS $body$
-BEGIN
-  -- The "0" corresponds to "OrthancPluginResourceType_Patient"
-  IF new.resourceType = 0 THEN
-    PERFORM PatientAddedOrUpdated(new.internalId, 0);
-  END IF;
-  RETURN NULL;
-END;
-$body$ LANGUAGE plpgsql;
-
-DROP TRIGGER IF EXISTS PatientAdded on Resources;
-CREATE TRIGGER PatientAdded
-AFTER INSERT ON Resources
-FOR EACH ROW
-EXECUTE PROCEDURE PatientAddedFunc();
-
--- initial population of 
-SELECT setval('patientrecyclingorder_seq_seq', MAX(seq)) FROM PatientRecyclingOrder;
-DELETE FROM GlobalIntegers WHERE key = 7;
-        -- UPDATE GlobalIntegers SET value = value + 1 WHERE key = 7 RETURNING value INTO newSeq;
-
--- INSERT INTO GlobalIntegers
---     SELECT 7, CAST(COALESCE(MAX(seq), 0) AS BIGINT) FROM PatientRecyclingOrder
---     ON CONFLICT DO NOTHING;
-
-
-------------------- ResourceDeleted trigger -------------------
-DROP TRIGGER IF EXISTS ResourceDeleted ON Resources;
-
--- The following trigger combines 2 triggers from SQLite:
--- ResourceDeleted + ResourceDeletedParentCleaning
-CREATE OR REPLACE FUNCTION ResourceDeletedFunc() 
-RETURNS TRIGGER AS $body$
-BEGIN
-  -- RAISE NOTICE 'ResourceDeletedFunc %', old.publicId;
-  INSERT INTO DeletedResources VALUES (old.resourceType, old.publicId);
-  
-  -- If this resource is the latest child, delete the parent
-  DELETE FROM Resources WHERE internalId = old.parentId
-                              AND NOT EXISTS (SELECT 1 FROM Resources WHERE parentId = old.parentId);
-  RETURN NULL;
-END;
-$body$ LANGUAGE plpgsql;
-
-DROP TRIGGER IF EXISTS ResourceDeleted on Resources;
-CREATE TRIGGER ResourceDeleted
-AFTER DELETE ON Resources
-FOR EACH ROW
-EXECUTE PROCEDURE ResourceDeletedFunc();
-
 
 ------------------- DeleteResource function -------------------
 
@@ -248,15 +180,20 @@
     OUT remaining_anncestor_public_id TEXT) AS $body$
 
 DECLARE
-    deleted_row RECORD;
-    locked_row RECORD;
+    deleted_resource_row RECORD;
+    deleted_parent_row RECORD;
+    deleted_grand_parent_row RECORD;
+    deleted_grand_grand_parent_row RECORD;
+
+    locked_parent_row RECORD;
+    locked_resource_row RECORD;
 
 BEGIN
 
     SET client_min_messages = warning;   -- suppress NOTICE:  relation "deletedresources" already exists, skipping
-    
-    -- note: temporary tables are created at session (connection) level -> they are likely to exist
-    -- these tables are used by the triggers
+
+    -- note: temporary tables are created at connection level -> they are likely to exist.
+    -- These tables are used by the triggers
     CREATE TEMPORARY TABLE IF NOT EXISTS DeletedResources(
         resourceType INTEGER NOT NULL,
         publicId VARCHAR(64) NOT NULL
@@ -264,28 +201,104 @@
 
     RESET client_min_messages;
 
-    -- clear the temporary table in case it has been created earlier in the session
+    -- clear the temporary table in case it has been created earlier in the connection
     DELETE FROM DeletedResources;
-    
+
     -- create/clear the DeletedFiles temporary table
     PERFORM CreateDeletedFilesTemporaryTable();
 
+
     -- Before deleting an object, we need to lock its parent until the end of the transaction to avoid that
     -- 2 threads deletes the last 2 instances of a series at the same time -> none of them would realize
     -- that they are deleting the last instance and the parent resources would not be deleted.
     -- Locking only the immediate parent is sufficient to prevent from this.
-    SELECT * INTO locked_row FROM resources WHERE internalid = (SELECT parentid FROM resources WHERE internalid = id) FOR UPDATE;
+    SELECT * INTO locked_parent_row FROM resources WHERE internalid = (SELECT parentid FROM resources WHERE internalid = id) FOR UPDATE;
+
+    -- Before deleting the resource itself, we lock it to retrieve the resourceType and to make sure not 2 connections try to
+    -- delete it at the same time
+    SELECT * INTO locked_resource_row FROM resources WHERE internalid = id FOR UPDATE;
+
+    -- before delete the resource itself, we must delete its grand-grand-children, the grand-children and its children no to violate 
+    -- the parentId referencing an existing primary key constrain.  This is actually implementing the ON DELETE CASCADE that was on the parentId in previous revisions.
+    
+    -- If this resource has grand-grand-children, delete them
+    if locked_resource_row.resourceType < 1 THEN
+        WITH grand_grand_children_to_delete AS (SELECT grandGrandChildLevel.internalId, grandGrandChildLevel.resourceType, grandGrandChildLevel.publicId
+                                                FROM Resources childLevel
+                                                INNER JOIN Resources grandChildLevel ON childLevel.internalId = grandChildLevel.parentId
+                                                INNER JOIN Resources grandGrandChildLevel ON grandChildLevel.internalId = grandGrandChildLevel.parentId
+                                                WHERE childLevel.parentId = id),
+        
+        deleted_grand_grand_children_rows AS (DELETE FROM Resources WHERE internalId IN (SELECT internalId FROM grand_grand_children_to_delete)
+                                              RETURNING resourceType, publicId)
+
+        INSERT INTO DeletedResources SELECT resourceType, publicId FROM deleted_grand_grand_children_rows; 
+    END IF;
+
+    -- If this resource has grand-children, delete them
+    if locked_resource_row.resourceType < 2 THEN
+        WITH grand_children_to_delete AS (SELECT grandChildLevel.internalId, grandChildLevel.resourceType, grandChildLevel.publicId
+                                          FROM Resources childLevel
+                                          INNER JOIN Resources grandChildLevel ON childLevel.internalId = grandChildLevel.parentId
+                                          WHERE childLevel.parentId = id),
+        
+        deleted_grand_children_rows AS (DELETE FROM Resources WHERE internalId IN (SELECT internalId FROM grand_children_to_delete)
+                                        RETURNING resourceType, publicId)
+
+        INSERT INTO DeletedResources SELECT resourceType, publicId FROM deleted_grand_children_rows; 
+    END IF;
+
+    -- If this resource has children, delete them
+    if locked_resource_row.resourceType < 3 THEN
+        WITH deleted_children AS (DELETE FROM Resources 
+                                  WHERE parentId = id
+                                  RETURNING resourceType, publicId)
+        INSERT INTO DeletedResources SELECT resourceType, publicId FROM deleted_children; 
+    END IF;
+
 
     -- delete the resource itself
-    DELETE FROM Resources WHERE internalId=id RETURNING * INTO deleted_row;
-    -- note: there is a ResourceDeletedFunc trigger that will execute here and delete the parents if there are no remaining children + 
+    DELETE FROM Resources WHERE internalId=id RETURNING * INTO deleted_resource_row;
 
+    -- keep track of the deleted resources for C++ code
+    INSERT INTO DeletedResources VALUES (deleted_resource_row.resourceType, deleted_resource_row.publicId);
+  
     -- If this resource still has siblings, keep track of the remaining parent
     -- (a parent that must not be deleted but whose LastUpdate must be updated)
     SELECT resourceType, publicId INTO remaining_ancestor_resource_type, remaining_anncestor_public_id
         FROM Resources 
-        WHERE internalId = deleted_row.parentId
-            AND EXISTS (SELECT 1 FROM Resources WHERE parentId = deleted_row.parentId);
+        WHERE internalId = deleted_resource_row.parentId
+            AND EXISTS (SELECT 1 FROM Resources WHERE parentId = deleted_resource_row.parentId);
+
+	IF deleted_resource_row.resourceType > 0 THEN
+        -- If this resource is the latest child, delete the parent
+        DELETE FROM Resources WHERE internalId = deleted_resource_row.parentId
+                                    AND NOT EXISTS (SELECT 1 FROM Resources WHERE parentId = deleted_resource_row.parentId)
+                                    RETURNING * INTO deleted_parent_row;
+        IF FOUND THEN
+            INSERT INTO DeletedResources VALUES (deleted_parent_row.resourceType, deleted_parent_row.publicId);
+
+            IF deleted_parent_row.resourceType > 0 THEN
+                -- If this resource is the latest child, delete the parent
+                DELETE FROM Resources WHERE internalId = deleted_parent_row.parentId
+                                    AND NOT EXISTS (SELECT 1 FROM Resources WHERE parentId = deleted_parent_row.parentId)
+                                    RETURNING * INTO deleted_grand_parent_row;
+                IF FOUND THEN
+                    INSERT INTO DeletedResources VALUES (deleted_grand_parent_row.resourceType, deleted_grand_parent_row.publicId);
+
+                    IF deleted_grand_parent_row.resourceType > 0 THEN
+                        -- If this resource is the latest child, delete the parent
+                        DELETE FROM Resources WHERE internalId = deleted_grand_parent_row.parentId
+                                            AND NOT EXISTS (SELECT 1 FROM Resources WHERE parentId = deleted_grand_parent_row.parentId)
+                                            RETURNING * INTO deleted_grand_parent_row;
+                        IF FOUND THEN
+                            INSERT INTO DeletedResources VALUES (deleted_grand_parent_row.resourceType, deleted_grand_parent_row.publicId);
+                        END IF;
+                    END IF;
+                END IF;
+            END IF;
+        END IF;
+    END IF;
 
 END;
 
@@ -296,9 +309,9 @@
 
 BEGIN
 
-    SET client_min_messages = warning;   -- suppress NOTICE:  relation "deletedresources" already exists, skipping
-    
-    -- note: temporary tables are created at session (connection) level -> they are likely to exist
+    SET client_min_messages = warning;   -- suppress NOTICE:  relation "DeletedFiles" already exists, skipping
+
+    -- note: temporary tables created at connection level -> they are likely to exist
     CREATE TEMPORARY TABLE IF NOT EXISTS DeletedFiles(
         uuid VARCHAR(64) NOT NULL,
         fileType INTEGER,
@@ -313,13 +326,14 @@
 
     RESET client_min_messages;
 
-    -- clear the temporary table in case it has been created earlier in the session
+    -- clear the temporary table in case it has been created earlier in the connection
     DELETE FROM DeletedFiles;
 END;
 
 $body$ LANGUAGE plpgsql;
 
-
+-- Keep track of deleted files such that the C++ code knows which files have been deleted.
+-- Attached files are deleted by cascade when the related resource is deleted.
 CREATE OR REPLACE FUNCTION AttachedFileDeletedFunc() 
 RETURNS TRIGGER AS $body$
 BEGIN
@@ -339,6 +353,19 @@
 EXECUTE PROCEDURE AttachedFileDeletedFunc();
 
 
+CREATE OR REPLACE FUNCTION DeleteAttachment(
+    IN resource_id BIGINT,
+    IN file_type INTEGER) 
+RETURNS VOID AS $body$
+BEGIN
+    -- create/clear the DeletedFiles temporary table
+    PERFORM CreateDeletedFilesTemporaryTable();
+
+    DELETE FROM AttachedFiles WHERE id = resource_id AND fileType = file_type;
+END;
+$body$ LANGUAGE plpgsql;
+
+
 ------------------- Fast Statistics -------------------
 
 -- initial population of GlobalIntegers if not already there
@@ -538,50 +565,82 @@
   OUT instance_internal_id BIGINT) AS $body$
 
 BEGIN
+	-- Assume the parent series already exists to minimize exceptions.  
+    -- Most of the instances are not the first of their series - especially when we need high performances.
+
 	is_new_patient := 1;
 	is_new_study := 1;
 	is_new_series := 1;
 	is_new_instance := 1;
 
-	BEGIN
-        INSERT INTO "resources" VALUES (DEFAULT, 0, patient_public_id, NULL, 0) RETURNING internalid INTO patient_internal_id;
-    EXCEPTION
-        WHEN unique_violation THEN
-            is_new_patient := 0;
-            SELECT internalid INTO patient_internal_id FROM "resources" WHERE publicId = patient_public_id FOR UPDATE;  -- also locks the resource and its parent to prevent from deletion while we complete this transaction
-    END;
+	-- First, check if the series already exists
+	SELECT internalid INTO series_internal_id FROM "resources" WHERE publicId = series_public_id;
+
+	IF series_internal_id IS NOT NULL THEN
+	    -- RAISE NOTICE 'series-found %', series_internal_id;
+		is_new_patient := 0;
+		is_new_study := 0;
+		is_new_series := 0;
 
-	BEGIN
-        INSERT INTO "resources" VALUES (DEFAULT, 1, study_public_id, patient_internal_id, 0) RETURNING internalid INTO study_internal_id;
-    EXCEPTION
-        WHEN unique_violation THEN
-            is_new_study := 0;
-            SELECT internalid INTO study_internal_id FROM "resources" WHERE publicId = study_public_id FOR UPDATE;  -- also locks the resource and its parent to prevent from deletion while we complete this transaction    END;
-    END;
+		-- If the series exists, insert the instance directly
+		BEGIN
+			INSERT INTO "resources" VALUES (DEFAULT, 3, instance_public_id, series_internal_id, 0) RETURNING internalid INTO instance_internal_id;
+		EXCEPTION
+			WHEN unique_violation THEN
+				is_new_instance := 0;
+				SELECT internalid INTO instance_internal_id FROM "resources" WHERE publicId = instance_public_id;
+		END;
+
+    	SELECT internalid INTO patient_internal_id FROM "resources" WHERE publicId = patient_public_id;
+		SELECT internalid INTO study_internal_id FROM "resources" WHERE publicId = study_public_id;
+
+	ELSE
+	    -- RAISE NOTICE 'series-not-found';
 
-	BEGIN
-	    INSERT INTO "resources" VALUES (DEFAULT, 2, series_public_id, study_internal_id, 0) RETURNING internalid INTO series_internal_id;
-    EXCEPTION
-        WHEN unique_violation THEN
-            is_new_series := 0;
-            SELECT internalid INTO series_internal_id FROM "resources" WHERE publicId = series_public_id FOR UPDATE;  -- also locks the resource and its parent to prevent from deletion while we complete this transaction    END;
-    END;
+		-- If the series does not exist, execute the "full" steps
+		BEGIN
+			INSERT INTO "resources" VALUES (DEFAULT, 0, patient_public_id, NULL, 0) RETURNING internalid INTO patient_internal_id;
+		EXCEPTION
+			WHEN unique_violation THEN
+				is_new_patient := 0;
+				SELECT internalid INTO patient_internal_id FROM "resources" WHERE publicId = patient_public_id;
+		END;
+	
+		BEGIN
+			INSERT INTO "resources" VALUES (DEFAULT, 1, study_public_id, patient_internal_id, 0) RETURNING internalid INTO study_internal_id;
+		EXCEPTION
+			WHEN unique_violation THEN
+				is_new_study := 0;
+				SELECT internalid INTO study_internal_id FROM "resources" WHERE publicId = study_public_id;
+		END;
+	
+		BEGIN
+			INSERT INTO "resources" VALUES (DEFAULT, 2, series_public_id, study_internal_id, 0) RETURNING internalid INTO series_internal_id;
+		EXCEPTION
+			WHEN unique_violation THEN
+				is_new_series := 0;
+				SELECT internalid INTO series_internal_id FROM "resources" WHERE publicId = series_public_id;
+		END;
+	
+		BEGIN
+			INSERT INTO "resources" VALUES (DEFAULT, 3, instance_public_id, series_internal_id, 0) RETURNING internalid INTO instance_internal_id;
+		EXCEPTION
+			WHEN unique_violation THEN
+				is_new_instance := 0;
+				SELECT internalid INTO instance_internal_id FROM "resources" WHERE publicId = instance_public_id;
+		END;
 
-  	BEGIN
-		INSERT INTO "resources" VALUES (DEFAULT, 3, instance_public_id, series_internal_id, 0) RETURNING internalid INTO instance_internal_id;
-    EXCEPTION
-        WHEN unique_violation THEN
-            is_new_instance := 0;
-            SELECT internalid INTO instance_internal_id FROM "resources" WHERE publicId = instance_public_id FOR UPDATE;  -- also locks the resource and its parent to prevent from deletion while we complete this transaction
-    END;
+	END IF;
+
 
-    IF is_new_instance > 0 THEN
-        -- Move the patient to the end of the recycling order.
-        PERFORM PatientAddedOrUpdated(patient_internal_id, 1);
-    END IF;  
+	IF is_new_instance > 0 THEN
+		-- Move the patient to the end of the recycling order.
+		PERFORM PatientAddedOrUpdated(patient_internal_id);
+	END IF;
 END;
 $body$ LANGUAGE plpgsql;
 
+
 -- function to compute a statistic in a ReadOnly transaction
 CREATE OR REPLACE FUNCTION ComputeStatisticsReadOnly(
     IN statistics_key INTEGER,
@@ -738,11 +797,38 @@
 
 CREATE INDEX QueuesIndex ON Queues (queueId, id);
 
+-- new in rev 599
+
+CREATE SEQUENCE IF NOT EXISTS PatientRecyclingOrderSequence INCREMENT 1 START 1;
+
+CREATE OR REPLACE FUNCTION ProtectPatient(patient_id BIGINT)
+RETURNS VOID AS $$
+BEGIN
+    INSERT INTO Metadata (id, type, value, revision) -- 18 = IsProtected
+    VALUES (patient_id, 18, 'true', 0)
+    ON CONFLICT (id, type)
+    DO UPDATE SET value = EXCLUDED.value, revision = EXCLUDED.revision;
+END;
+$$ LANGUAGE plpgsql;
+
+-- remove IsProtected and update PatientRecyclingOrder
+CREATE OR REPLACE FUNCTION UnprotectPatient(patient_id BIGINT)
+RETURNS VOID AS $$
+BEGIN
+    DELETE FROM Metadata WHERE id = patient_id AND type = 18; -- 18 = IsProtected
+
+    INSERT INTO Metadata (id, type, value, revision)
+    VALUES (patient_id, 19, nextval('PatientRecyclingOrderSequence')::TEXT, 0)
+    ON CONFLICT (id, type)
+    DO UPDATE SET value = EXCLUDED.value, revision = EXCLUDED.revision;
+END;
+$$ LANGUAGE plpgsql;
+
 
 -- set the global properties that actually documents the DB version, revision and some of the capabilities
 DELETE FROM GlobalProperties WHERE property IN (1, 4, 6, 10, 11, 12, 13, 14);
 INSERT INTO GlobalProperties VALUES (1, 6); -- GlobalProperty_DatabaseSchemaVersion
-INSERT INTO GlobalProperties VALUES (4, 5); -- GlobalProperty_DatabasePatchLevel
+INSERT INTO GlobalProperties VALUES (4, 599); -- GlobalProperty_DatabasePatchLevel
 INSERT INTO GlobalProperties VALUES (6, 1); -- GlobalProperty_GetTotalSizeIsFast
 INSERT INTO GlobalProperties VALUES (10, 1); -- GlobalProperty_HasTrigramIndex
 INSERT INTO GlobalProperties VALUES (11, 3); -- GlobalProperty_HasCreateInstance  -- this is actually the 3rd version of HasCreateInstance
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/PostgreSQL/Plugins/SQL/Upgrades/Rev5ToRev599.sql	Fri Jun 27 15:29:59 2025 +0200
@@ -0,0 +1,59 @@
+CREATE SEQUENCE IF NOT EXISTS PatientRecyclingOrderSequence INCREMENT 1 START 1;
+
+-- the protection mechanisms changed in rev 499.  We now use a metadata (18: IsProtected)
+-- while, in the past, patients where protected by not appearing in the PatientRecyclingOrder
+
+-- Step 1: Identify all patients that are not in PatientRecyclingOrder (those are the protected patients)
+-- The "0" corresponds to "OrthancPluginResourceType_Patient"
+WITH ProtectedPatients AS (
+    SELECT r.internalId AS internalId
+    FROM Resources r
+    LEFT JOIN PatientRecyclingOrder pro ON r.internalId = pro.patientId
+    WHERE r.resourceType = 0
+    AND pro.patientId IS NULL
+) 
+, UnprotectedPatients AS (
+    SELECT patientId AS internalId
+    FROM PatientRecyclingOrder
+    ORDER BY seq ASC
+)
+
+-- Step 2: Prepare the data for both metadata types
+, MetadataToInsert AS (
+    -- mark protected patient in 18: IsProtected
+    SELECT internalId, 18 AS metadataType, 'true' AS metadataValue
+    FROM ProtectedPatients    
+
+    UNION ALL
+
+    -- copy previous recycling order in 19: RecyclingOrder
+    SELECT internalId, 19 AS metadataType, nextval('PatientRecyclingOrderSequence')::TEXT AS metadataValue
+    FROM UnprotectedPatients    
+)
+
+-- Step 3: Insert the Metadata (since the metadata are new, there should not be any conflicts)
+INSERT INTO Metadata (id, type, value, revision)
+SELECT internalId, metadataType, metadataValue, 0
+FROM MetadataToInsert
+ON CONFLICT (id, type)
+DO UPDATE SET value = EXCLUDED.value, revision = EXCLUDED.revision;
+
+-- The PatientRecyclingOrder table can now be removed
+
+DROP TABLE PatientRecyclingOrder;
+
+DROP TRIGGER IF EXISTS PatientAdded on Resources;
+DROP FUNCTION IF EXISTS PatientAddedFunc;
+DROP FUNCTION IF EXISTS PatientAddedOrUpdated;
+
+-- The DeletedResources trigger is not used anymore
+
+DROP TRIGGER IF EXISTS ResourceDeleted ON Resources;
+DROP FUNCTION IF EXISTS ResourceDeletedFunc();
+
+-- The ON DELETE CASCADE on the Resources.parentId has been removed since this is now implemented 
+-- in the DeleteResource function
+-- Drop the existing foreign key constraint and add a new one without ON DELETE CASCADE in a single command
+ALTER TABLE Resources
+DROP CONSTRAINT IF EXISTS resources_parentid_fkey,
+ADD CONSTRAINT resources_parentid_fkey FOREIGN KEY (parentId) REFERENCES Resources(internalId);
\ No newline at end of file
--- a/Resources/CMake/DatabasesPluginConfiguration.cmake	Tue Jun 17 12:41:37 2025 +0200
+++ b/Resources/CMake/DatabasesPluginConfiguration.cmake	Fri Jun 27 15:29:59 2025 +0200
@@ -113,10 +113,12 @@
 
 list(APPEND DATABASES_SOURCES
   ${ORTHANC_CORE_SOURCES}
+  ${ORTHANC_DATABASES_ROOT}/Framework/Plugins/BaseIndexConnectionsPool.cpp
   ${ORTHANC_DATABASES_ROOT}/Framework/Plugins/DatabaseBackendAdapterV2.cpp
   ${ORTHANC_DATABASES_ROOT}/Framework/Plugins/DatabaseBackendAdapterV3.cpp
   ${ORTHANC_DATABASES_ROOT}/Framework/Plugins/DatabaseBackendAdapterV4.cpp
   ${ORTHANC_DATABASES_ROOT}/Framework/Plugins/DatabaseConstraint.cpp
+  ${ORTHANC_DATABASES_ROOT}/Framework/Plugins/DynamicIndexConnectionsPool.cpp
   ${ORTHANC_DATABASES_ROOT}/Framework/Plugins/ISqlLookupFormatter.cpp
   ${ORTHANC_DATABASES_ROOT}/Framework/Plugins/IndexBackend.cpp
   ${ORTHANC_DATABASES_ROOT}/Framework/Plugins/IndexConnectionsPool.cpp