changeset 849:6177e8e6be50

merged pg-next-1099 -> default
author Alain Mazy <am@orthanc.team>
date Fri, 18 Sep 2026 14:41:29 +0200
parents 2ef3d2ce4905 (current diff) 3e4d8da97292 (diff)
children d8d0b7b0cc75 8e0c16a006c8
files PostgreSQL/CMakeLists.txt PostgreSQL/NEWS PostgreSQL/Plugins/PostgreSQLIndex.cpp PostgreSQL/Plugins/SQL/Downgrades/Rev1099ToRev10.sql PostgreSQL/Plugins/SQL/Downgrades/Rev11ToRev10.sql PostgreSQL/Plugins/SQL/PrepareIndex.sql PostgreSQL/Plugins/SQL/Upgrades/Rev10ToRev1099.sql PostgreSQL/Plugins/SQL/Upgrades/Rev10ToRev11.sql
diffstat 8 files changed, 170 insertions(+), 13 deletions(-) [+]
line wrap: on
line diff
--- a/Framework/Plugins/ISqlLookupFormatter.cpp	Wed Aug 19 10:09:23 2026 +0200
+++ b/Framework/Plugins/ISqlLookupFormatter.cpp	Fri Sep 18 14:41:29 2026 +0200
@@ -807,6 +807,11 @@
     assert(upperLevel <= queryLevel &&
            queryLevel <= lowerLevel);
 
+    const bool isOrthancIdentifiersDefined = (!request.orthanc_id_patient().empty() ||
+                                              !request.orthanc_id_study().empty() ||
+                                              !request.orthanc_id_series().empty() ||
+                                              !request.orthanc_id_instance().empty());
+
     std::string orderingSql;
     std::string orderingJoins;
 
@@ -878,7 +883,14 @@
     }
     else
     {
-      orderingSql = "ROW_NUMBER() OVER (ORDER BY " + strQueryLevel + ".publicId) AS rowNumber";  // we need a default ordering in order to make default queries repeatable when using since&limit
+      if (isOrthancIdentifiersDefined && DetectLevel(request) == queryLevel)
+      { // this is a single resource, no need for ordering (ordering may prevents a lot of optimizations from the query planner)
+        orderingSql = "0 AS rowNumber";
+      }
+      else
+      {
+        orderingSql = "ROW_NUMBER() OVER (ORDER BY " + strQueryLevel + ".publicId) AS rowNumber";  // we need a default ordering in order to make default queries repeatable when using since&limit
+      }
     }
 
     sql = ("SELECT " +
@@ -890,11 +902,6 @@
 
     std::string joins, comparisons;
 
-    const bool isOrthancIdentifiersDefined = (!request.orthanc_id_patient().empty() ||
-                                              !request.orthanc_id_study().empty() ||
-                                              !request.orthanc_id_series().empty() ||
-                                              !request.orthanc_id_instance().empty());
-
     // handle parent constraints
     if (isOrthancIdentifiersDefined && Orthanc::IsResourceLevelAboveOrEqual(DetectLevel(request), queryLevel))
     {
--- a/Framework/PostgreSQL/PostgreSQLDatabase.cpp	Wed Aug 19 10:09:23 2026 +0200
+++ b/Framework/PostgreSQL/PostgreSQLDatabase.cpp	Fri Sep 18 14:41:29 2026 +0200
@@ -189,7 +189,7 @@
       PQclear(result);
 
       LOG(ERROR) << "PostgreSQL error: " << message;
-      ThrowException(false);
+      ThrowException(true);
     }
   }
 
--- a/PostgreSQL/CMakeLists.txt	Wed Aug 19 10:09:23 2026 +0200
+++ b/PostgreSQL/CMakeLists.txt	Fri Sep 18 14:41:29 2026 +0200
@@ -96,7 +96,8 @@
   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_REV6    ${CMAKE_SOURCE_DIR}/Plugins/SQL/Upgrades/Rev5ToRev6.sql
-  POSTGRESQL_UPGRADE_REV6_TO_REV10  ${CMAKE_SOURCE_DIR}/Plugins/SQL/Upgrades/Rev6ToRev10.sql
+  POSTGRESQL_UPGRADE_REV6_TO_REV10   ${CMAKE_SOURCE_DIR}/Plugins/SQL/Upgrades/Rev6ToRev10.sql
+  POSTGRESQL_UPGRADE_REV10_TO_REV11  ${CMAKE_SOURCE_DIR}/Plugins/SQL/Upgrades/Rev10ToRev11.sql
   )
 
 
--- a/PostgreSQL/NEWS	Wed Aug 19 10:09:23 2026 +0200
+++ b/PostgreSQL/NEWS	Fri Sep 18 14:41:29 2026 +0200
@@ -1,3 +1,14 @@
+Pending changes in the mainline
+===============================
+
+Changes:
+* Added an index on AttachedFiles uuids to speed-up the SDK calls 
+  SetAttachmentCustomData and GetAttachmentCustomData that are used by,
+  notably, the advanced-storage plugin.
+* Small update of the "DeleteResource" function to avoid a warning when multiple
+  clients are trying to delete the same resource at the same time.
+
+
 Release 10.3 (2026-08-19)
 =========================
 
--- a/PostgreSQL/Plugins/PostgreSQLIndex.cpp	Wed Aug 19 10:09:23 2026 +0200
+++ b/PostgreSQL/Plugins/PostgreSQLIndex.cpp	Fri Sep 18 14:41:29 2026 +0200
@@ -49,7 +49,7 @@
   static const GlobalProperty GlobalProperty_HasComputeStatisticsReadOnly = GlobalProperty_DatabaseInternal4;
 }
 
-#define CURRENT_DB_REVISION 10
+#define CURRENT_DB_REVISION 11
 
 namespace OrthancDatabases
 {
@@ -283,6 +283,19 @@
             currentRevision = 10;
           }
 
+          if (currentRevision == 10)
+          {
+            LOG(WARNING) << "Upgrading DB schema from revision 10 to revision 11";
+
+            std::string query;
+
+            Orthanc::EmbeddedResources::GetFileResource
+              (query, Orthanc::EmbeddedResources::POSTGRESQL_UPGRADE_REV10_TO_REV11);
+            t.GetDatabaseTransaction().ExecuteMultiLines(query);
+            hasAppliedAnUpgrade = true;
+            currentRevision = 11;
+          }
+
           if (hasAppliedAnUpgrade)
           {
             LOG(WARNING) << "Upgrading DB schema by applying PrepareIndex.sql";
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/PostgreSQL/Plugins/SQL/Downgrades/Rev11ToRev10.sql	Fri Sep 18 14:41:29 2026 +0200
@@ -0,0 +1,114 @@
+-- restore the old 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_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 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 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_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_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_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;
+
+DROP INDEX IF EXISTS AttachedFilesUuid;
+
+-- 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);
+INSERT INTO GlobalProperties VALUES (4, 10); -- GlobalProperty_DatabasePatchLevel
--- a/PostgreSQL/Plugins/SQL/PrepareIndex.sql	Wed Aug 19 10:09:23 2026 +0200
+++ b/PostgreSQL/Plugins/SQL/PrepareIndex.sql	Fri Sep 18 14:41:29 2026 +0200
@@ -260,9 +260,11 @@
     -- delete the resource itself
     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 FOUND THEN
+        -- keep track of the deleted resources for C++ code
+        INSERT INTO DeletedResources VALUES (deleted_resource_row.resourceType, deleted_resource_row.publicId);
+    END IF;
+
     -- 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
@@ -852,12 +854,16 @@
 
 CREATE INDEX IF NOT EXISTS InvalidChildCountsId ON InvalidChildCounts (id); -- see https://discourse.orthanc-server.org/t/increase-in-cpu-usage-of-database-after-update-to-orthanc-1-12-7/6057/6
 
+-- new in rev 11
 
+CREATE INDEX IF NOT EXISTS AttachedFilesUuid ON AttachedFiles (uuid);
+
+----------------------------------
 
 -- 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, 10); -- GlobalProperty_DatabasePatchLevel
+INSERT INTO GlobalProperties VALUES (4, 11); -- 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/Rev10ToRev11.sql	Fri Sep 18 14:41:29 2026 +0200
@@ -0,0 +1,5 @@
+-- Update from Rev 10 to Rev 11
+
+-- the DeleteResource function is updated in PrepareIndex.sql
+-- the AttachedFilesUuid index is created in PrepareIndex.sql
+SELECT 1;
\ No newline at end of file