changeset 710:2f2036e0f352 sql-opti tip

renamed revision 599 into 6
author Alain Mazy <am@orthanc.team>
date Tue, 22 Jul 2025 10:22:34 +0200 (2 days ago)
parents a88a2776fea4
children
files Framework/Plugins/IDatabaseBackend.h Framework/Plugins/IndexBackend.cpp PostgreSQL/CMakeLists.txt PostgreSQL/Plugins/PostgreSQLIndex.cpp PostgreSQL/Plugins/SQL/Downgrades/Rev599ToRev5.sql PostgreSQL/Plugins/SQL/Downgrades/Rev6ToRev5.sql PostgreSQL/Plugins/SQL/PrepareIndex.sql PostgreSQL/Plugins/SQL/Upgrades/Rev5ToRev599.sql PostgreSQL/Plugins/SQL/Upgrades/Rev5ToRev6.sql
diffstat 9 files changed, 321 insertions(+), 321 deletions(-) [+]
line wrap: on
line diff
--- a/Framework/Plugins/IDatabaseBackend.h	Thu Jul 17 12:35:36 2025 +0200
+++ b/Framework/Plugins/IDatabaseBackend.h	Tue Jul 22 10:22:34 2025 +0200
@@ -39,14 +39,14 @@
   public:
     struct AuditLog
     {
-      uint64_t timeStamp;
+      std::string timeStamp;
       std::string userId;
       OrthancPluginResourceType resourceType;
       std::string resourceId;
       std::string action;
       std::string logData;
 
-      AuditLog(uint64_t timeStamp,
+      AuditLog(const std::string& timeStamp,
                const std::string& userId,
                OrthancPluginResourceType resourceType,
                const std::string& resourceId,
--- a/Framework/Plugins/IndexBackend.cpp	Thu Jul 17 12:35:36 2025 +0200
+++ b/Framework/Plugins/IndexBackend.cpp	Tue Jul 22 10:22:34 2025 +0200
@@ -4736,7 +4736,7 @@
       LookupFormatter formatter(manager.GetDialect());
       std::vector<std::string> filters;
 
-      std::string sql = "SELECT ts, userId, resourceType, resourceId, action, logData FROM AuditLogs ";
+      std::string sql = "SELECT to_char(ts, 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"'), userId, resourceType, resourceId, action, logData FROM AuditLogs ";
 
       if (!userIdFilter.empty())
       {
@@ -4789,7 +4789,7 @@
           throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
         }
         
-        statement.SetResultFieldType(0, ValueType_Integer64);
+        statement.SetResultFieldType(0, ValueType_Utf8String);
         statement.SetResultFieldType(1, ValueType_Utf8String);
         statement.SetResultFieldType(2, ValueType_Integer64);
         statement.SetResultFieldType(3, ValueType_Utf8String);
@@ -4798,7 +4798,7 @@
 
         while (!statement.IsDone())
         {
-          logs.push_back(AuditLog(statement.ReadInteger64(0),
+          logs.push_back(AuditLog(statement.ReadString(0),
                                   statement.ReadString(1),
                                   static_cast<OrthancPluginResourceType>(statement.ReadInteger64(2)),
                                   statement.ReadString(3),
--- a/PostgreSQL/CMakeLists.txt	Thu Jul 17 12:35:36 2025 +0200
+++ b/PostgreSQL/CMakeLists.txt	Tue Jul 22 10:22:34 2025 +0200
@@ -95,7 +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
+  POSTGRESQL_UPGRADE_REV5_TO_REV6  ${CMAKE_SOURCE_DIR}/Plugins/SQL/Upgrades/Rev5ToRev6.sql
   )
 
 
--- a/PostgreSQL/Plugins/PostgreSQLIndex.cpp	Thu Jul 17 12:35:36 2025 +0200
+++ b/PostgreSQL/Plugins/PostgreSQLIndex.cpp	Tue Jul 22 10:22:34 2025 +0200
@@ -49,7 +49,7 @@
   static const GlobalProperty GlobalProperty_HasComputeStatisticsReadOnly = GlobalProperty_DatabaseInternal4;
 }
 
-#define CURRENT_DB_REVISION 599
+#define CURRENT_DB_REVISION 6
 
 namespace OrthancDatabases
 {
@@ -250,15 +250,15 @@
 
           if (currentRevision == 5)
           {
-            LOG(WARNING) << "Upgrading DB schema from revision 5 to revision 599";
+            LOG(WARNING) << "Upgrading DB schema from revision 5 to revision 6";
 
             std::string query;
 
             Orthanc::EmbeddedResources::GetFileResource
-              (query, Orthanc::EmbeddedResources::POSTGRESQL_UPGRADE_REV5_TO_REV599);
+              (query, Orthanc::EmbeddedResources::POSTGRESQL_UPGRADE_REV5_TO_REV6);
             t.GetDatabaseTransaction().ExecuteMultiLines(query);
             hasAppliedAnUpgrade = true;
-            currentRevision = 599;
+            currentRevision = 6;
           }
 
           if (hasAppliedAnUpgrade)
--- a/PostgreSQL/Plugins/SQL/Downgrades/Rev599ToRev5.sql	Thu Jul 17 12:35:36 2025 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,250 +0,0 @@
--- 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;
-
-
--- Remove the AuditLogs table
------------
-
-DROP INDEX IF EXISTS AuditLogsUserId;
-DROP INDEX IF EXISTS AuditLogsResourceId;
-DROP INDEX IF EXISTS AuditLogsAction;
-DROP TABLE IF EXISTS AuditLogs;
-
-
-----------
-
--- 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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/PostgreSQL/Plugins/SQL/Downgrades/Rev6ToRev5.sql	Tue Jul 22 10:22:34 2025 +0200
@@ -0,0 +1,250 @@
+-- This file contains an SQL procedure to downgrade from schema 6 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;
+
+
+-- Remove the AuditLogs table
+-----------
+
+DROP INDEX IF EXISTS AuditLogsUserId;
+DROP INDEX IF EXISTS AuditLogsResourceId;
+DROP INDEX IF EXISTS AuditLogsAction;
+DROP TABLE IF EXISTS AuditLogs;
+
+
+----------
+
+-- 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	Thu Jul 17 12:35:36 2025 +0200
+++ b/PostgreSQL/Plugins/SQL/PrepareIndex.sql	Tue Jul 22 10:22:34 2025 +0200
@@ -797,7 +797,7 @@
 
 CREATE INDEX IF NOT EXISTS QueuesIndex ON Queues (queueId, id);
 
--- new in rev 599
+-- new in rev 6
 
 CREATE SEQUENCE IF NOT EXISTS PatientRecyclingOrderSequence INCREMENT 1 START 1;
 
@@ -842,7 +842,7 @@
 -- 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, 599); -- GlobalProperty_DatabasePatchLevel
+INSERT INTO GlobalProperties VALUES (4, 6); -- 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
--- a/PostgreSQL/Plugins/SQL/Upgrades/Rev5ToRev599.sql	Thu Jul 17 12:35:36 2025 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,59 +0,0 @@
-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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/PostgreSQL/Plugins/SQL/Upgrades/Rev5ToRev6.sql	Tue Jul 22 10:22:34 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