changeset 581:a80775ee5eea find-refactoring

MySQL: Added support for ExtendedFind
author Alain Mazy <am@orthanc.team>
date Mon, 21 Oct 2024 14:34:57 +0200
parents 35d2df9572b1
children 8296c6a0238e
files Framework/Common/DatabaseManager.h Framework/Common/Query.cpp Framework/Common/StatementId.cpp Framework/Common/StatementId.h Framework/MySQL/MySQLStatement.cpp Framework/Plugins/ISqlLookupFormatter.h Framework/Plugins/IndexBackend.cpp MySQL/NEWS MySQL/Plugins/MySQLIndex.cpp MySQL/Plugins/MySQLIndex.h
diffstat 10 files changed, 329 insertions(+), 268 deletions(-) [+]
line wrap: on
line diff
--- a/Framework/Common/DatabaseManager.h	Tue Oct 15 15:52:39 2024 +0200
+++ b/Framework/Common/DatabaseManager.h	Mon Oct 21 14:34:57 2024 +0200
@@ -194,6 +194,8 @@
       {
         IResult::Print(stream, GetResult());
       }
+
+      virtual void Execute(const Dictionary& parameters) = 0;
     };
 
 
@@ -221,7 +223,7 @@
         Execute(parameters);
       }
 
-      void Execute(const Dictionary& parameters);
+      virtual void Execute(const Dictionary& parameters);
 
       void ExecuteWithoutResult()
       {
@@ -253,7 +255,7 @@
         Execute(parameters);
       }
 
-      void Execute(const Dictionary& parameters);
+      virtual void Execute(const Dictionary& parameters);
 
       void ExecuteWithoutResult()
       {
--- a/Framework/Common/Query.cpp	Tue Oct 15 15:52:39 2024 +0200
+++ b/Framework/Common/Query.cpp	Mon Oct 21 14:34:57 2024 +0200
@@ -76,7 +76,7 @@
       parameter = parameter.substr(2, parameter.size() - 3);
 
       tokens_.push_back(new Token(true, parameter));
-      parameters_[parameter] = ValueType_Null;
+      parameters_[parameter] = ValueType_Utf8String;
 
       last = it->second;
 
--- a/Framework/Common/StatementId.cpp	Tue Oct 15 15:52:39 2024 +0200
+++ b/Framework/Common/StatementId.cpp	Mon Oct 21 14:34:57 2024 +0200
@@ -24,21 +24,31 @@
 #include "StatementId.h"
 
 #include <string.h>
+#include <Toolbox.h>
 
 namespace OrthancDatabases
 {
   bool StatementId::operator< (const StatementId& other) const
   {
-    if (line_ != other.line_)
-    {
-      return line_ < other.line_;
-    }
+    return hash_ < other.hash_;
+  }
+
+  StatementId::StatementId(const char* file,
+                           int line) :
+      file_(file),
+      line_(line)
+  {
+    Orthanc::Toolbox::ComputeMD5(hash_, file_ + boost::lexical_cast<std::string>(line_));
+  }
 
-    if (strcmp(file_, other.file_) < 0)
-    {
-      return true;
-    }
+  StatementId::StatementId(const char* file,
+                           int line,
+                           const std::string& statement) :
+      file_(file),
+      line_(line),
+      statement_(statement)
+  {
+    Orthanc::Toolbox::ComputeMD5(hash_, file_ + boost::lexical_cast<std::string>(line_) + statement_);
+  }
 
-    return statement_ < other.statement_;
-  }
 }
--- a/Framework/Common/StatementId.h	Tue Oct 15 15:52:39 2024 +0200
+++ b/Framework/Common/StatementId.h	Mon Oct 21 14:34:57 2024 +0200
@@ -37,25 +37,17 @@
     const char* file_;
     int line_;
     std::string statement_;
+    std::string hash_;
 
     StatementId(); // Forbidden
     
   public:
     StatementId(const char* file,
-                int line) :
-      file_(file),
-      line_(line)
-    {
-    }
+                int line);
 
     StatementId(const char* file,
                 int line,
-                const std::string& statement) :
-      file_(file),
-      line_(line),
-      statement_(statement)
-    {
-    }
+                const std::string& statement);
 
     const char* GetFile() const
     {
--- a/Framework/MySQL/MySQLStatement.cpp	Tue Oct 15 15:52:39 2024 +0200
+++ b/Framework/MySQL/MySQLStatement.cpp	Mon Oct 21 14:34:57 2024 +0200
@@ -152,6 +152,10 @@
           buffer_.resize(8);
           break;
 
+        case MYSQL_TYPE_NULL:
+          orthancType_ = ValueType_Null;
+          break;
+
         case MYSQL_TYPE_STRING:
         case MYSQL_TYPE_VAR_STRING:
         case MYSQL_TYPE_BLOB:
--- a/Framework/Plugins/ISqlLookupFormatter.h	Tue Oct 15 15:52:39 2024 +0200
+++ b/Framework/Plugins/ISqlLookupFormatter.h	Mon Oct 21 14:34:57 2024 +0200
@@ -62,6 +62,8 @@
 
     virtual std::string FormatLimits(uint64_t since, uint64_t count) = 0;
 
+    virtual std::string FormatNull(const char* type) = 0;
+
     /**
      * Whether to escape '[' and ']', which is only needed for
      * MSSQL. New in Orthanc 1.10.0, from the following changeset:
--- a/Framework/Plugins/IndexBackend.cpp	Tue Oct 15 15:52:39 2024 +0200
+++ b/Framework/Plugins/IndexBackend.cpp	Mon Oct 21 14:34:57 2024 +0200
@@ -2191,6 +2191,23 @@
       }
     }
 
+    virtual std::string FormatNull(const char* type)
+    {
+      switch (dialect_)
+      {
+        case Dialect_PostgreSQL:
+          return std::string("NULL::") + type;
+        case Dialect_MSSQL:
+        case Dialect_SQLite:
+        case Dialect_MySQL:
+          return "NULL";
+
+        default:
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+      }
+    }
+
+
     virtual std::string FormatLimits(uint64_t since, uint64_t count)
     {
       std::string sql;
@@ -2210,7 +2227,6 @@
         }; break;
         case Dialect_SQLite:
         case Dialect_PostgreSQL:
-        case Dialect_MySQL:
         {
           if (count > 0)
           {
@@ -2221,6 +2237,21 @@
             sql += " OFFSET " + boost::lexical_cast<std::string>(since);
           }
         }; break;
+        case Dialect_MySQL:
+        {
+          if (count > 0 && since > 0)
+          {
+            sql += " LIMIT " + boost::lexical_cast<std::string>(since) + ", " + boost::lexical_cast<std::string>(count);
+          }
+          else if (count > 0)
+          {
+            sql += " LIMIT " + boost::lexical_cast<std::string>(count);
+          }
+          else if (since > 0)
+          {
+            sql += " LIMIT " + boost::lexical_cast<std::string>(since) + ", 18446744073709551615"; // max uint64 value when you don't want any limit
+          }
+        }; break;
         default:
           throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
       }
@@ -3199,8 +3230,7 @@
 
     sql = "WITH Lookup AS (" + lookupSql + ") SELECT COUNT(*) FROM Lookup";
 
-    DatabaseManager::StandaloneStatement statement(manager, sql);  // TODO-FIND: cache dynamic statement ?  Probably worth it since it can be very complex queries !
-    formatter.PrepareStatement(statement);
+    DatabaseManager::CachedStatement statement(STATEMENT_FROM_HERE_DYNAMIC(sql), manager, sql);
     statement.Execute(formatter.GetDictionary());
     response.mutable_count_resources()->set_count(statement.ReadInteger64(0));
   }
@@ -3224,38 +3254,78 @@
 
     // extract the resource id of interest by executing the lookup in a CTE
     LookupFormatter formatter(manager.GetDialect());
-    std::string lookupSql;
-    ISqlLookupFormatter::Apply(lookupSql, formatter, request);
+    std::string lookupSqlCTE;
+    ISqlLookupFormatter::Apply(lookupSqlCTE, formatter, request);
 
     // base query, retrieve the ordered internalId and publicId of the selected resources
-    sql = "WITH Lookup AS (" + lookupSql + ") "
-          "SELECT "
+    sql = "WITH Lookup AS (" + lookupSqlCTE + ") ";
+
+    std::string oneInstanceSqlCTE;
+
+    if (request.level() != Orthanc::DatabasePluginMessages::ResourceType::RESOURCE_INSTANCE &&
+        request.retrieve_one_instance_metadata_and_attachments())
+    {
+      switch (request.level())
+      {
+        case Orthanc::DatabasePluginMessages::ResourceType::RESOURCE_SERIES:
+        {
+          oneInstanceSqlCTE = "SELECT Lookup.internalId AS parentInternalId, childLevel.publicId AS instancePublicId, childLevel.internalId AS instanceInternalId, ROW_NUMBER() OVER (PARTITION BY Lookup.internalId ORDER BY childLevel.publicId) AS rowNum"
+                "   FROM Resources AS childLevel "
+                "   INNER JOIN Lookup ON childLevel.parentId = Lookup.internalId";
+        }; break;
+        case Orthanc::DatabasePluginMessages::ResourceType::RESOURCE_STUDY:
+        {
+          oneInstanceSqlCTE = "SELECT Lookup.internalId AS parentInternalId, grandChildLevel.publicId AS instancePublicId, grandChildLevel.internalId AS instanceInternalId, ROW_NUMBER() OVER (PARTITION BY Lookup.internalId ORDER BY grandChildLevel.publicId) AS rowNum"
+                "   FROM Resources AS grandChildLevel "
+                "   INNER JOIN Resources childLevel ON grandChildLevel.parentId = childLevel.internalId "
+                "   INNER JOIN Lookup ON childLevel.parentId = Lookup.internalId";
+        }; break;
+        case Orthanc::DatabasePluginMessages::ResourceType::RESOURCE_PATIENT:
+        {
+          oneInstanceSqlCTE = "SELECT Lookup.internalId AS parentInternalId, grandGrandChildLevel.publicId AS instancePublicId, grandGrandChildLevel.internalId AS instanceInternalId, ROW_NUMBER() OVER (PARTITION BY Lookup.internalId ORDER BY grandGrandChildLevel.publicId) AS rowNum"
+                "   FROM Resources AS grandGrandChildLevel "
+                "   INNER JOIN Resources grandChildLevel ON grandGrandChildLevel.parentId = grandChildLevel.internalId "
+                "   INNER JOIN Resources childLevel ON grandChildLevel.parentId = childLevel.internalId "
+                "   INNER JOIN Lookup ON childLevel.parentId = Lookup.internalId";
+        }; break;
+        default:
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }
+      sql += ", _OneInstance AS (" + oneInstanceSqlCTE + ") ";
+      sql += ", OneInstance AS (SELECT parentInternalId, instancePublicId, instanceInternalId FROM _OneInstance WHERE rowNum = 1) ";  // this is a generic way to implement DISTINCT ON
+    }
+
+    // if (!oneInstanceSqlCTE.empty() && (manager.GetDialect() == Dialect_MySQL || manager.GetDialect() == Dialect_SQLite))
+    // { // all CTEs must be declared first in some dialects
+    // }
+
+    sql += " SELECT "
           "  " TOSTRING(QUERY_LOOKUP) " AS c0_queryId, "
           "  Lookup.internalId AS c1_internalId, "
           "  Lookup.rowNumber AS c2_rowNumber, "
           "  Lookup.publicId AS c3_string1, "
-          "  NULL::TEXT AS c4_string2, "
-          "  NULL::TEXT AS c5_string3, "
-          "  NULL::INT AS c6_int1, "
-          "  NULL::INT AS c7_int2, "
-          "  NULL::BIGINT AS c8_big_int1, "
-          "  NULL::BIGINT AS c9_big_int2 "
+          "  " + formatter.FormatNull("TEXT") + " AS c4_string2, "
+          "  " + formatter.FormatNull("TEXT") + " AS c5_string3, "
+          "  " + formatter.FormatNull("INT") + " AS c6_int1, "
+          "  " + formatter.FormatNull("INT") + " AS c7_int2, "
+          "  " + formatter.FormatNull("BIGINT") + " AS c8_big_int1, "
+          "  " + formatter.FormatNull("BIGINT") + " AS c9_big_int2 "
           "  FROM Lookup ";
 
     // need MainDicomTags from resource ?
     if (request.retrieve_main_dicom_tags())
     {
-      sql += "UNION SELECT "
+      sql += "UNION ALL SELECT "
              "  " TOSTRING(QUERY_MAIN_DICOM_TAGS) " AS c0_queryId, "
              "  Lookup.internalId AS c1_internalId, "
-             "  NULL::BIGINT AS c2_rowNumber, "
+             "  " + formatter.FormatNull("BIGINT") + " AS c2_rowNumber, "
              "  value AS c3_string1, "
-             "  NULL::TEXT AS c4_string2, "
-             "  NULL::TEXT AS c5_string3, "
+             "  " + formatter.FormatNull("TEXT") + " AS c4_string2, "
+             "  " + formatter.FormatNull("TEXT") + " AS c5_string3, "
              "  tagGroup AS c6_int1, "
              "  tagElement AS c7_int2, "
-             "  NULL::BIGINT AS c8_big_int1, "
-             "  NULL::BIGINT AS c9_big_int2 "
+             "  " + formatter.FormatNull("BIGINT") + " AS c8_big_int1, "
+             "  " + formatter.FormatNull("BIGINT") + " AS c9_big_int2 "
              "FROM Lookup "
              "INNER JOIN MainDicomTags ON MainDicomTags.id = Lookup.internalId ";
     }
@@ -3263,17 +3333,17 @@
     // need resource metadata ?
     if (request.retrieve_metadata())
     {
-      sql += "UNION SELECT "
+      sql += "UNION ALL SELECT "
              "  " TOSTRING(QUERY_METADATA) " AS c0_queryId, "
              "  Lookup.internalId AS c1_internalId, "
-             "  NULL::BIGINT AS c2_rowNumber, "
+             "  " + formatter.FormatNull("BIGINT") + " AS c2_rowNumber, "
              "  value AS c3_string1, "
-             "  NULL::TEXT AS c4_string2, "
-             "  NULL::TEXT AS c5_string3, "
+             "  " + formatter.FormatNull("TEXT") + " AS c4_string2, "
+             "  " + formatter.FormatNull("TEXT") + " AS c5_string3, "
              "  type AS c6_int1, "
-             "  NULL::INT AS c7_int2, "
-             "  NULL::BIGINT AS c8_big_int1, "
-             "  NULL::BIGINT AS c9_big_int2 "
+             "  " + formatter.FormatNull("INT") + " AS c7_int2, "
+             "  " + formatter.FormatNull("BIGINT") + " AS c8_big_int1, "
+             "  " + formatter.FormatNull("BIGINT") + " AS c9_big_int2 "
              "FROM Lookup "
              "INNER JOIN Metadata ON Metadata.id = Lookup.internalId ";
     }
@@ -3281,10 +3351,10 @@
     // need resource attachments ?
     if (request.retrieve_attachments())
     {
-      sql += "UNION SELECT "
+      sql += "UNION ALL SELECT "
              "  " TOSTRING(QUERY_ATTACHMENTS) " AS c0_queryId, "
              "  Lookup.internalId AS c1_internalId, "
-             "  NULL::BIGINT AS c2_rowNumber, "
+             "  " + formatter.FormatNull("BIGINT") + " AS c2_rowNumber, "
              "  uuid AS c3_string1, "
              "  uncompressedHash AS c4_string2, "
              "  compressedHash AS c5_string3, "
@@ -3299,17 +3369,17 @@
     // need resource labels ?
     if (request.retrieve_labels())
     {
-      sql += "UNION SELECT "
+      sql += "UNION ALL SELECT "
              "  " TOSTRING(QUERY_LABELS) " AS c0_queryId, "
              "  Lookup.internalId AS c1_internalId, "
-             "  NULL::BIGINT AS c2_rowNumber, "
+             "  " + formatter.FormatNull("BIGINT") + " AS c2_rowNumber, "
              "  label AS c3_string1, "
-             "  NULL::TEXT AS c4_string2, "
-             "  NULL::TEXT AS c5_string3, "
-             "  NULL::INT AS c6_int1, "
-             "  NULL::INT AS c7_int2, "
-             "  NULL::BIGINT AS c8_big_int1, "
-             "  NULL::BIGINT AS c9_big_int2 "
+             "  " + formatter.FormatNull("TEXT") + " AS c4_string2, "
+             "  " + formatter.FormatNull("TEXT") + " AS c5_string3, "
+             "  " + formatter.FormatNull("INT") + " AS c6_int1, "
+             "  " + formatter.FormatNull("INT") + " AS c7_int2, "
+             "  " + formatter.FormatNull("BIGINT") + " AS c8_big_int1, "
+             "  " + formatter.FormatNull("BIGINT") + " AS c9_big_int2 "
              "FROM Lookup "
              "INNER JOIN Labels ON Labels.id = Lookup.internalId ";
     }
@@ -3336,17 +3406,17 @@
 
       if (parentSpec->retrieve_main_dicom_tags())
       {
-        sql += "UNION SELECT "
+        sql += "UNION ALL SELECT "
                "  " TOSTRING(QUERY_PARENT_MAIN_DICOM_TAGS) " AS c0_queryId, "
                "  Lookup.internalId AS c1_internalId, "
-               "  NULL::BIGINT AS c2_rowNumber, "
+               "  " + formatter.FormatNull("BIGINT") + " AS c2_rowNumber, "
                "  value AS c3_string1, "
-               "  NULL::TEXT AS c4_string2, "
-               "  NULL::TEXT AS c5_string3, "
+               "  " + formatter.FormatNull("TEXT") + " AS c4_string2, "
+               "  " + formatter.FormatNull("TEXT") + " AS c5_string3, "
                "  tagGroup AS c6_int1, "
                "  tagElement AS c7_int2, "
-               "  NULL::BIGINT AS c8_big_int1, "
-               "  NULL::BIGINT AS c9_big_int2 "
+               "  " + formatter.FormatNull("BIGINT") + " AS c8_big_int1, "
+               "  " + formatter.FormatNull("BIGINT") + " AS c9_big_int2 "
                "FROM Lookup "
                "INNER JOIN Resources currentLevel ON Lookup.internalId = currentLevel.internalId "
                "INNER JOIN MainDicomTags ON MainDicomTags.id = currentLevel.parentId ";
@@ -3354,17 +3424,17 @@
 
       if (parentSpec->retrieve_metadata())
       {
-        sql += "UNION SELECT "
+        sql += "UNION ALL SELECT "
                "  " TOSTRING(QUERY_PARENT_METADATA) " AS c0_queryId, "
                "  Lookup.internalId AS c1_internalId, "
-               "  NULL::BIGINT AS c2_rowNumber, "
+               "  " + formatter.FormatNull("BIGINT") + " AS c2_rowNumber, "
                "  value AS c3_string1, "
-               "  NULL::TEXT AS c4_string2, "
-               "  NULL::TEXT AS c5_string3, "
+               "  " + formatter.FormatNull("TEXT") + " AS c4_string2, "
+               "  " + formatter.FormatNull("TEXT") + " AS c5_string3, "
                "  type AS c6_int1, "
-               "  NULL::INT AS c7_int2, "
-               "  NULL::BIGINT AS c8_big_int1, "
-               "  NULL::BIGINT AS c9_big_int2 "
+               "  " + formatter.FormatNull("INT") + " AS c7_int2, "
+               "  " + formatter.FormatNull("BIGINT") + " AS c8_big_int1, "
+               "  " + formatter.FormatNull("BIGINT") + " AS c9_big_int2 "
                "FROM Lookup "
                "INNER JOIN Resources currentLevel ON Lookup.internalId = currentLevel.internalId "
                "INNER JOIN Metadata ON Metadata.id = currentLevel.parentId ";
@@ -3389,17 +3459,17 @@
 
         if (grandparentSpec->retrieve_main_dicom_tags())
         {
-        sql += "UNION SELECT "
+          sql += "UNION ALL SELECT "
                "  " TOSTRING(QUERY_GRAND_PARENT_MAIN_DICOM_TAGS) " AS c0_queryId, "
                "  Lookup.internalId AS c1_internalId, "
-               "  NULL::BIGINT AS c2_rowNumber, "
+               "  " + formatter.FormatNull("BIGINT") + " AS c2_rowNumber, "
                "  value AS c3_string1, "
-               "  NULL::TEXT AS c4_string2, "
-               "  NULL::TEXT AS c5_string3, "
+               "  " + formatter.FormatNull("TEXT") + " AS c4_string2, "
+               "  " + formatter.FormatNull("TEXT") + " AS c5_string3, "
                "  tagGroup AS c6_int1, "
                "  tagElement AS c7_int2, "
-               "  NULL::BIGINT AS c8_big_int1, "
-               "  NULL::BIGINT AS c9_big_int2 "
+               "  " + formatter.FormatNull("BIGINT") + " AS c8_big_int1, "
+               "  " + formatter.FormatNull("BIGINT") + " AS c9_big_int2 "
                "FROM Lookup "
                "INNER JOIN Resources currentLevel ON Lookup.internalId = currentLevel.internalId "
                "INNER JOIN Resources parentLevel ON currentLevel.parentId = parentLevel.internalId "
@@ -3408,17 +3478,17 @@
 
         if (grandparentSpec->retrieve_metadata())
         {
-          sql += "UNION SELECT "
+          sql += "UNION ALL SELECT "
                 "  " TOSTRING(QUERY_GRAND_PARENT_METADATA) " AS c0_queryId, "
                 "  Lookup.internalId AS c1_internalId, "
-                "  NULL::BIGINT AS c2_rowNumber, "
+                "  " + formatter.FormatNull("BIGINT") + " AS c2_rowNumber, "
                 "  value AS c3_string1, "
-                "  NULL::TEXT AS c4_string2, "
-                "  NULL::TEXT AS c5_string3, "
+                "  " + formatter.FormatNull("TEXT") + " AS c4_string2, "
+                "  " + formatter.FormatNull("TEXT") + " AS c5_string3, "
                 "  type AS c6_int1, "
-                "  NULL::INT AS c7_int2, "
-                "  NULL::BIGINT AS c8_big_int1, "
-                "  NULL::BIGINT AS c9_big_int2 "
+                "  " + formatter.FormatNull("INT") + " AS c7_int2, "
+                "  " + formatter.FormatNull("BIGINT") + " AS c8_big_int1, "
+                "  " + formatter.FormatNull("BIGINT") + " AS c9_big_int2 "
                 "FROM Lookup "
                 "INNER JOIN Resources currentLevel ON Lookup.internalId = currentLevel.internalId "
                 "INNER JOIN Resources parentLevel ON currentLevel.parentId = parentLevel.internalId "
@@ -3449,17 +3519,17 @@
 
       if (childrenSpec->retrieve_main_dicom_tags_size() > 0)
       {
-        sql += "UNION SELECT "
+        sql += "UNION ALL SELECT "
                "  " TOSTRING(QUERY_CHILDREN_MAIN_DICOM_TAGS) " AS c0_queryId, "
                "  Lookup.internalId AS c1_internalId, "
-               "  NULL::BIGINT AS c2_rowNumber, "
+               "  " + formatter.FormatNull("BIGINT") + " AS c2_rowNumber, "
                "  value AS c3_string1, "
-               "  NULL::TEXT AS c4_string2, "
-               "  NULL::TEXT AS c5_string3, "
+               "  " + formatter.FormatNull("TEXT") + " AS c4_string2, "
+               "  " + formatter.FormatNull("TEXT") + " AS c5_string3, "
                "  tagGroup AS c6_int1, "
                "  tagElement AS c7_int2, "
-               "  NULL::BIGINT AS c8_big_int1, "
-               "  NULL::BIGINT AS c9_big_int2 "
+               "  " + formatter.FormatNull("BIGINT") + " AS c8_big_int1, "
+               "  " + formatter.FormatNull("BIGINT") + " AS c9_big_int2 "
                "FROM Lookup "
                "  INNER JOIN Resources childLevel ON childLevel.parentId = Lookup.internalId "
                "  INNER JOIN MainDicomTags ON MainDicomTags.id = childLevel.internalId AND (tagGroup, tagElement) IN (" + JoinRequestedTags(childrenSpec) + ")";
@@ -3468,34 +3538,34 @@
       // need children identifiers ?
       if (childrenSpec->retrieve_identifiers())  
       {
-        sql += "UNION SELECT "
+        sql += "UNION ALL SELECT "
                "  " TOSTRING(QUERY_CHILDREN_IDENTIFIERS) " AS c0_queryId, "
                "  Lookup.internalId AS c1_internalId, "
-               "  NULL::BIGINT AS c2_rowNumber, "
+               "  " + formatter.FormatNull("BIGINT") + " AS c2_rowNumber, "
                "  childLevel.publicId AS c3_string1, "
-               "  NULL::TEXT AS c4_string2, "
-               "  NULL::TEXT AS c5_string3, "
-               "  NULL::INT AS c6_int1, "
-               "  NULL::INT AS c7_int2, "
-               "  NULL::BIGINT AS c8_big_int1, "
-               "  NULL::BIGINT AS c9_big_int2 "
+               "  " + formatter.FormatNull("TEXT") + " AS c4_string2, "
+               "  " + formatter.FormatNull("TEXT") + " AS c5_string3, "
+               "  " + formatter.FormatNull("INT") + " AS c6_int1, "
+               "  " + formatter.FormatNull("INT") + " AS c7_int2, "
+               "  " + formatter.FormatNull("BIGINT") + " AS c8_big_int1, "
+               "  " + formatter.FormatNull("BIGINT") + " AS c9_big_int2 "
                "FROM Lookup "
                "  INNER JOIN Resources childLevel ON Lookup.internalId = childLevel.parentId ";
       }
 
       if (childrenSpec->retrieve_metadata_size() > 0)
       {
-        sql += "UNION SELECT "
+        sql += "UNION ALL SELECT "
                 "  " TOSTRING(QUERY_CHILDREN_METADATA) " AS c0_queryId, "
                 "  Lookup.internalId AS c1_internalId, "
-                "  NULL::BIGINT AS c2_rowNumber, "
+                "  " + formatter.FormatNull("BIGINT") + " AS c2_rowNumber, "
                 "  value AS c3_string1, "
-                "  NULL::TEXT AS c4_string2, "
-                "  NULL::TEXT AS c5_string3, "
+                "  " + formatter.FormatNull("TEXT") + " AS c4_string2, "
+                "  " + formatter.FormatNull("TEXT") + " AS c5_string3, "
                 "  type AS c6_int1, "
-                "  NULL::INT AS c7_int2, "
-                "  NULL::BIGINT AS c8_big_int1, "
-                "  NULL::BIGINT AS c9_big_int2 "
+                "  " + formatter.FormatNull("INT") + " AS c7_int2, "
+                "  " + formatter.FormatNull("BIGINT") + " AS c8_big_int1, "
+                "  " + formatter.FormatNull("BIGINT") + " AS c9_big_int2 "
                 "FROM Lookup "
                 "  INNER JOIN Resources childLevel ON childLevel.parentId = Lookup.internalId "
                 "  INNER JOIN Metadata ON Metadata.id = childLevel.internalId AND Metadata.type IN (" + JoinRequestedMetadata(childrenSpec) + ") ";
@@ -3520,17 +3590,17 @@
         // need grand children identifiers ?
         if (grandchildrenSpec->retrieve_identifiers())  
         {
-          sql += "UNION SELECT "
+          sql += "UNION ALL SELECT "
                 "  " TOSTRING(QUERY_GRAND_CHILDREN_IDENTIFIERS) " AS c0_queryId, "
                 "  Lookup.internalId AS c1_internalId, "
-                "  NULL::BIGINT AS c2_rowNumber, "
+                "  " + formatter.FormatNull("BIGINT") + " AS c2_rowNumber, "
                 "  grandChildLevel.publicId AS c3_string1, "
-                "  NULL::TEXT AS c4_string2, "
-                "  NULL::TEXT AS c5_string3, "
-                "  NULL::INT AS c6_int1, "
-                "  NULL::INT AS c7_int2, "
-                "  NULL::BIGINT AS c8_big_int1, "
-                "  NULL::BIGINT AS c9_big_int2 "
+                "  " + formatter.FormatNull("TEXT") + " AS c4_string2, "
+                "  " + formatter.FormatNull("TEXT") + " AS c5_string3, "
+                "  " + formatter.FormatNull("INT") + " AS c6_int1, "
+                "  " + formatter.FormatNull("INT") + " AS c7_int2, "
+                "  " + formatter.FormatNull("BIGINT") + " AS c8_big_int1, "
+                "  " + formatter.FormatNull("BIGINT") + " AS c9_big_int2 "
                 "FROM Lookup "
                 "INNER JOIN Resources childLevel ON Lookup.internalId = childLevel.parentId "
                 "INNER JOIN Resources grandChildLevel ON childLevel.internalId = grandChildLevel.parentId ";
@@ -3538,17 +3608,17 @@
 
         if (grandchildrenSpec->retrieve_main_dicom_tags_size() > 0)
         {
-          sql += "UNION SELECT "
+          sql += "UNION ALL SELECT "
                  "  " TOSTRING(QUERY_GRAND_CHILDREN_MAIN_DICOM_TAGS) " AS c0_queryId, "
                  "  Lookup.internalId AS c1_internalId, "
-                 "  NULL::BIGINT AS c2_rowNumber, "
+                 "  " + formatter.FormatNull("BIGINT") + " AS c2_rowNumber, "
                  "  value AS c3_string1, "
-                 "  NULL::TEXT AS c4_string2, "
-                 "  NULL::TEXT AS c5_string3, "
+                 "  " + formatter.FormatNull("TEXT") + " AS c4_string2, "
+                 "  " + formatter.FormatNull("TEXT") + " AS c5_string3, "
                  "  tagGroup AS c6_int1, "
                  "  tagElement AS c7_int2, "
-                 "  NULL::BIGINT AS c8_big_int1, "
-                 "  NULL::BIGINT AS c9_big_int2 "
+                 "  " + formatter.FormatNull("BIGINT") + " AS c8_big_int1, "
+                 "  " + formatter.FormatNull("BIGINT") + " AS c9_big_int2 "
                  "FROM Lookup "
                  "  INNER JOIN Resources childLevel ON childLevel.parentId = Lookup.internalId "
                  "  INNER JOIN Resources grandChildLevel ON grandChildLevel.parentId = childLevel.internalId "
@@ -3557,17 +3627,17 @@
 
         if (grandchildrenSpec->retrieve_metadata_size() > 0)
         {
-          sql += "UNION SELECT "
+          sql += "UNION ALL SELECT "
                  "  " TOSTRING(QUERY_GRAND_CHILDREN_METADATA) " AS c0_queryId, "
                  "  Lookup.internalId AS c1_internalId, "
-                 "  NULL::BIGINT AS c2_rowNumber, "
+                 "  " + formatter.FormatNull("BIGINT") + " AS c2_rowNumber, "
                  "  value AS c3_string1, "
-                 "  NULL::TEXT AS c4_string2, "
-                 "  NULL::TEXT AS c5_string3, "
+                 "  " + formatter.FormatNull("TEXT") + " AS c4_string2, "
+                 "  " + formatter.FormatNull("TEXT") + " AS c5_string3, "
                  "  type AS c6_int1, "
-                 "  NULL::INT AS c7_int2, "
-                 "  NULL::BIGINT AS c8_big_int1, "
-                 "  NULL::BIGINT AS c9_big_int2 "
+                 "  " + formatter.FormatNull("INT") + " AS c7_int2, "
+                 "  " + formatter.FormatNull("BIGINT") + " AS c8_big_int1, "
+                 "  " + formatter.FormatNull("BIGINT") + " AS c9_big_int2 "
                  "FROM Lookup "
                  "  INNER JOIN Resources childLevel ON childLevel.parentId = Lookup.internalId "
                  "  INNER JOIN Resources grandChildLevel ON grandChildLevel.parentId = childLevel.internalId "
@@ -3581,17 +3651,17 @@
           // need grand children identifiers ?
           if (grandgrandchildrenSpec->retrieve_identifiers())  
           {
-            sql += "UNION SELECT "
+            sql += "UNION ALL SELECT "
                   "  " TOSTRING(QUERY_GRAND_GRAND_CHILDREN_IDENTIFIERS) " AS c0_queryId, "
                   "  Lookup.internalId AS c1_internalId, "
-                  "  NULL::BIGINT AS c2_rowNumber, "
+                  "  " + formatter.FormatNull("BIGINT") + " AS c2_rowNumber, "
                   "  grandGrandChildLevel.publicId AS c3_string1, "
-                  "  NULL::TEXT AS c4_string2, "
-                  "  NULL::TEXT AS c5_string3, "
-                  "  NULL::INT AS c6_int1, "
-                  "  NULL::INT AS c7_int2, "
-                  "  NULL::BIGINT AS c8_big_int1, "
-                  "  NULL::BIGINT AS c9_big_int2 "
+                  "  " + formatter.FormatNull("TEXT") + " AS c4_string2, "
+                  "  " + formatter.FormatNull("TEXT") + " AS c5_string3, "
+                  "  " + formatter.FormatNull("INT") + " AS c6_int1, "
+                  "  " + formatter.FormatNull("INT") + " AS c7_int2, "
+                  "  " + formatter.FormatNull("BIGINT") + " AS c8_big_int1, "
+                  "  " + formatter.FormatNull("BIGINT") + " AS c9_big_int2 "
                   "FROM Lookup "
                   "INNER JOIN Resources childLevel ON Lookup.internalId = childLevel.parentId "
                   "INNER JOIN Resources grandChildLevel ON childLevel.internalId = grandChildLevel.parentId "
@@ -3604,17 +3674,17 @@
     // need parent identifier ?
     if (request.retrieve_parent_identifier())
     {
-      sql += "UNION SELECT "
+      sql += "UNION ALL SELECT "
              "  " TOSTRING(QUERY_PARENT_IDENTIFIER) " AS c0_queryId, "
              "  Lookup.internalId AS c1_internalId, "
-             "  NULL::BIGINT AS c2_rowNumber, "
+             "  " + formatter.FormatNull("BIGINT") + " AS c2_rowNumber, "
              "  parentLevel.publicId AS c3_string1, "
-             "  NULL::TEXT AS c4_string2, "
-             "  NULL::TEXT AS c5_string3, "
-             "  NULL::INT AS c6_int1, "
-             "  NULL::INT AS c7_int2, "
-             "  NULL::BIGINT AS c8_big_int1, "
-             "  NULL::BIGINT AS c9_big_int2 "
+             "  " + formatter.FormatNull("TEXT") + " AS c4_string2, "
+             "  " + formatter.FormatNull("TEXT") + " AS c5_string3, "
+             "  " + formatter.FormatNull("INT") + " AS c6_int1, "
+             "  " + formatter.FormatNull("INT") + " AS c7_int2, "
+             "  " + formatter.FormatNull("BIGINT") + " AS c8_big_int1, "
+             "  " + formatter.FormatNull("BIGINT") + " AS c9_big_int2 "
              "FROM Lookup "
              "  INNER JOIN Resources currentLevel ON currentLevel.internalId = Lookup.internalId "
              "  INNER JOIN Resources parentLevel ON currentLevel.parentId = parentLevel.internalId ";
@@ -3624,68 +3694,37 @@
     if (request.level() != Orthanc::DatabasePluginMessages::ResourceType::RESOURCE_INSTANCE &&
         request.retrieve_one_instance_metadata_and_attachments())
     {
-      // Here, we create a nested CTE 'OneInstance' with one instance ID to join with metadata and main
-      sql += "UNION"
-             "  (WITH OneInstance AS";
-      
-      switch (request.level())
-      {
-        case Orthanc::DatabasePluginMessages::ResourceType::RESOURCE_SERIES:
-        {
-          sql+= "  (SELECT DISTINCT ON (Lookup.internalId) Lookup.internalId AS parentInternalId, childLevel.publicId AS instancePublicId, childLevel.internalId AS instanceInternalId"
-                "   FROM Resources AS childLevel "
-                "   INNER JOIN Lookup ON childLevel.parentId = Lookup.internalId) ";
-        }; break;
-        case Orthanc::DatabasePluginMessages::ResourceType::RESOURCE_STUDY:
-        {
-          sql+= "  (SELECT DISTINCT ON (Lookup.internalId) Lookup.internalId AS parentInternalId, grandChildLevel.publicId AS instancePublicId, grandChildLevel.internalId AS instanceInternalId"
-                "   FROM Resources AS grandChildLevel "
-                "   INNER JOIN Resources childLevel ON grandChildLevel.parentId = childLevel.internalId "
-                "   INNER JOIN Lookup ON childLevel.parentId = Lookup.internalId) ";
-        }; break;
-        case Orthanc::DatabasePluginMessages::ResourceType::RESOURCE_PATIENT:
-        {
-          sql+= "  (SELECT DISTINCT ON (Lookup.internalId) Lookup.internalId AS parentInternalId, grandGrandChildLevel.publicId AS instancePublicId, grandGrandChildLevel.internalId AS instanceInternalId"
-                "   FROM Resources AS grandGrandChildLevel "
-                "   INNER JOIN Resources grandChildLevel ON grandGrandChildLevel.parentId = grandChildLevel.internalId "
-                "   INNER JOIN Resources childLevel ON grandChildLevel.parentId = childLevel.internalId "
-                "   INNER JOIN Lookup ON childLevel.parentId = Lookup.internalId) ";
-        }; break;
-        default:
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
-      }
-
-      sql += "   SELECT"
+      sql += "   UNION ALL SELECT"
              "    " TOSTRING(QUERY_ONE_INSTANCE_IDENTIFIER) " AS c0_queryId, "
              "    parentInternalId AS c1_internalId, "
-             "    NULL::BIGINT AS c2_rowNumber, "
+             "    " + formatter.FormatNull("BIGINT") + " AS c2_rowNumber, "
              "    instancePublicId AS c3_string1, "
-             "    NULL::TEXT AS c4_string2, "
-             "    NULL::TEXT AS c5_string3, "
-             "    NULL::INT AS c6_int1, "
-             "    NULL::INT AS c7_int2, "
+             "    " + formatter.FormatNull("TEXT") + " AS c4_string2, "
+             "    " + formatter.FormatNull("TEXT") + " AS c5_string3, "
+             "    " + formatter.FormatNull("INT") + " AS c6_int1, "
+             "    " + formatter.FormatNull("INT") + " AS c7_int2, "
              "    instanceInternalId AS c8_big_int1, "
-             "    NULL::BIGINT AS c9_big_int2 "
+             "    " + formatter.FormatNull("BIGINT") + " AS c9_big_int2 "
              "   FROM OneInstance ";
 
-      sql += "   UNION SELECT"
+      sql += "   UNION ALL SELECT"
              "    " TOSTRING(QUERY_ONE_INSTANCE_METADATA) " AS c0_queryId, "
              "    parentInternalId AS c1_internalId, "
-             "    NULL::BIGINT AS c2_rowNumber, "
+             "    " + formatter.FormatNull("BIGINT") + " AS c2_rowNumber, "
              "    Metadata.value AS c3_string1, "
-             "    NULL::TEXT AS c4_string2, "
-             "    NULL::TEXT AS c5_string3, "
+             "    " + formatter.FormatNull("TEXT") + " AS c4_string2, "
+             "    " + formatter.FormatNull("TEXT") + " AS c5_string3, "
              "    Metadata.type AS c6_int1, "
-             "    NULL::INT AS c7_int2, "
-             "    NULL::BIGINT AS c8_big_int1, "
-             "    NULL::BIGINT AS c9_big_int2 "
+             "    " + formatter.FormatNull("INT") + " AS c7_int2, "
+             "    " + formatter.FormatNull("BIGINT") + " AS c8_big_int1, "
+             "    " + formatter.FormatNull("BIGINT") + " AS c9_big_int2 "
              "   FROM Metadata "
              "   INNER JOIN OneInstance ON Metadata.id = OneInstance.instanceInternalId";
              
-      sql += "   UNION SELECT"
+      sql += "   UNION ALL SELECT"
              "    " TOSTRING(QUERY_ONE_INSTANCE_ATTACHMENTS) " AS c0_queryId, "
              "    parentInternalId AS c1_internalId, "
-             "    NULL::BIGINT AS c2_rowNumber, "
+             "    " + formatter.FormatNull("BIGINT") + " AS c2_rowNumber, "
              "    uuid AS c3_string1, "
              "    uncompressedHash AS c4_string2, "
              "    compressedHash AS c5_string3, "
@@ -3696,36 +3735,47 @@
              "   FROM AttachedFiles "
              "   INNER JOIN OneInstance ON AttachedFiles.id = OneInstance.instanceInternalId";
 
-      sql += "  ) ";
+      // sql += "  ) ";
 
     }
 
     sql += " ORDER BY c0_queryId, c2_rowNumber";  // this is really important to make sure that the Lookup query is the first one to provide results since we use it to create the responses element !
 
-    DatabaseManager::StandaloneStatement statement(manager, sql);  // TODO-FIND: cache dynamic statement ?  Probably worth it since it can be very complex queries !
-    formatter.PrepareStatement(statement);
-    statement.Execute(formatter.GetDictionary());
+    std::unique_ptr<DatabaseManager::StatementBase> statement;
+    if (manager.GetDialect() == Dialect_MySQL)
+    { // TODO: investigate why "complex" cached statement do not seem to work properly in MySQL
+      statement.reset(new DatabaseManager::StandaloneStatement(manager, sql));
+    }
+    else
+    {
+      statement.reset(new DatabaseManager::CachedStatement(STATEMENT_FROM_HERE_DYNAMIC(sql), manager, sql));
+    }
     
+    statement->Execute(formatter.GetDictionary());
+    
+    // LOG(INFO) << sql;
 
     std::map<int64_t, Orthanc::DatabasePluginMessages::Find_Response*> responses;
 
-    while (!statement.IsDone())
+    while (!statement->IsDone())
     {
-      int32_t queryId = statement.ReadInteger32(C0_QUERY_ID);
-      int64_t internalId = statement.ReadInteger64(C1_INTERNAL_ID);
+      int32_t queryId = statement->ReadInteger32(C0_QUERY_ID);
+      int64_t internalId = statement->ReadInteger64(C1_INTERNAL_ID);
       
       assert(queryId == QUERY_LOOKUP || responses.find(internalId) != responses.end()); // the QUERY_LOOKUP must be read first and must create the response before any other query tries to populate the fields
 
+      // LOG(INFO) << queryId << "  " << statement->ReadString(C3_STRING_1);
+
       switch (queryId)
       {
         case QUERY_LOOKUP:
           responses[internalId] = response.add_find();
-          responses[internalId]->set_public_id(statement.ReadString(C3_STRING_1));
+          responses[internalId]->set_public_id(statement->ReadString(C3_STRING_1));
           responses[internalId]->set_internal_id(internalId);
           break;
 
         case QUERY_LABELS:
-          responses[internalId]->add_labels(statement.ReadString(C3_STRING_1));
+          responses[internalId]->add_labels(statement->ReadString(C3_STRING_1));
           break;
 
         case QUERY_MAIN_DICOM_TAGS:
@@ -3733,9 +3783,9 @@
           Orthanc::DatabasePluginMessages::Find_Response_ResourceContent* content = GetResourceContent(responses[internalId], request.level());
           Orthanc::DatabasePluginMessages::Find_Response_Tag* tag = content->add_main_dicom_tags();
 
-          tag->set_value(statement.ReadString(C3_STRING_1));
-          tag->set_group(statement.ReadInteger32(C6_INT_1));
-          tag->set_element(statement.ReadInteger32(C7_INT_2));
+          tag->set_value(statement->ReadString(C3_STRING_1));
+          tag->set_group(statement->ReadInteger32(C6_INT_1));
+          tag->set_element(statement->ReadInteger32(C7_INT_2));
           }; break;
 
         case QUERY_PARENT_MAIN_DICOM_TAGS:
@@ -3743,9 +3793,9 @@
           Orthanc::DatabasePluginMessages::Find_Response_ResourceContent* content = GetResourceContent(responses[internalId], static_cast<Orthanc::DatabasePluginMessages::ResourceType>(request.level() - 1));
           Orthanc::DatabasePluginMessages::Find_Response_Tag* tag = content->add_main_dicom_tags();
 
-          tag->set_value(statement.ReadString(C3_STRING_1));
-          tag->set_group(statement.ReadInteger32(C6_INT_1));
-          tag->set_element(statement.ReadInteger32(C7_INT_2));
+          tag->set_value(statement->ReadString(C3_STRING_1));
+          tag->set_group(statement->ReadInteger32(C6_INT_1));
+          tag->set_element(statement->ReadInteger32(C7_INT_2));
         }; break;
 
         case QUERY_GRAND_PARENT_MAIN_DICOM_TAGS:
@@ -3753,24 +3803,24 @@
           Orthanc::DatabasePluginMessages::Find_Response_ResourceContent* content = GetResourceContent(responses[internalId], static_cast<Orthanc::DatabasePluginMessages::ResourceType>(request.level() - 2));
           Orthanc::DatabasePluginMessages::Find_Response_Tag* tag = content->add_main_dicom_tags();
 
-          tag->set_value(statement.ReadString(C3_STRING_1));
-          tag->set_group(statement.ReadInteger32(C6_INT_1));
-          tag->set_element(statement.ReadInteger32(C7_INT_2));
+          tag->set_value(statement->ReadString(C3_STRING_1));
+          tag->set_group(statement->ReadInteger32(C6_INT_1));
+          tag->set_element(statement->ReadInteger32(C7_INT_2));
         }; break;
 
         case QUERY_CHILDREN_IDENTIFIERS:
         {
           Orthanc::DatabasePluginMessages::Find_Response_ChildrenContent* content = GetChildrenContent(responses[internalId], static_cast<Orthanc::DatabasePluginMessages::ResourceType>(request.level() + 1));
-          content->add_identifiers(statement.ReadString(C3_STRING_1));
+          content->add_identifiers(statement->ReadString(C3_STRING_1));
         }; break;
 
         case QUERY_CHILDREN_MAIN_DICOM_TAGS:
         {
           Orthanc::DatabasePluginMessages::Find_Response_ChildrenContent* content = GetChildrenContent(responses[internalId], static_cast<Orthanc::DatabasePluginMessages::ResourceType>(request.level() + 1));
           Orthanc::DatabasePluginMessages::Find_Response_MultipleTags* tag = content->add_main_dicom_tags();
-          tag->add_values(statement.ReadString(C3_STRING_1)); // TODO: handle sequences ??
-          tag->set_group(statement.ReadInteger32(C6_INT_1));
-          tag->set_element(statement.ReadInteger32(C7_INT_2));
+          tag->add_values(statement->ReadString(C3_STRING_1)); // TODO: handle sequences ??
+          tag->set_group(statement->ReadInteger32(C6_INT_1));
+          tag->set_element(statement->ReadInteger32(C7_INT_2));
         }; break;
 
         case QUERY_CHILDREN_METADATA:
@@ -3778,14 +3828,14 @@
           Orthanc::DatabasePluginMessages::Find_Response_ChildrenContent* content = GetChildrenContent(responses[internalId], static_cast<Orthanc::DatabasePluginMessages::ResourceType>(request.level() + 1));
           Orthanc::DatabasePluginMessages::Find_Response_MultipleMetadata* metadata = content->add_metadata();
 
-          metadata->add_values(statement.ReadString(C3_STRING_1));
-          metadata->set_key(statement.ReadInteger32(C6_INT_1));
+          metadata->add_values(statement->ReadString(C3_STRING_1));
+          metadata->set_key(statement->ReadInteger32(C6_INT_1));
         }; break;
 
         case QUERY_GRAND_CHILDREN_IDENTIFIERS:
         {
           Orthanc::DatabasePluginMessages::Find_Response_ChildrenContent* content = GetChildrenContent(responses[internalId], static_cast<Orthanc::DatabasePluginMessages::ResourceType>(request.level() + 2));
-          content->add_identifiers(statement.ReadString(C3_STRING_1));
+          content->add_identifiers(statement->ReadString(C3_STRING_1));
         }; break;
 
         case QUERY_GRAND_CHILDREN_MAIN_DICOM_TAGS:
@@ -3793,9 +3843,9 @@
           Orthanc::DatabasePluginMessages::Find_Response_ChildrenContent* content = GetChildrenContent(responses[internalId], static_cast<Orthanc::DatabasePluginMessages::ResourceType>(request.level() + 2));
           Orthanc::DatabasePluginMessages::Find_Response_MultipleTags* tag = content->add_main_dicom_tags();
 
-          tag->add_values(statement.ReadString(C3_STRING_1)); // TODO: handle sequences ??
-          tag->set_group(statement.ReadInteger32(C6_INT_1));
-          tag->set_element(statement.ReadInteger32(C7_INT_2));
+          tag->add_values(statement->ReadString(C3_STRING_1)); // TODO: handle sequences ??
+          tag->set_group(statement->ReadInteger32(C6_INT_1));
+          tag->set_element(statement->ReadInteger32(C7_INT_2));
         }; break;
 
         case QUERY_GRAND_CHILDREN_METADATA:
@@ -3803,27 +3853,27 @@
           Orthanc::DatabasePluginMessages::Find_Response_ChildrenContent* content = GetChildrenContent(responses[internalId], static_cast<Orthanc::DatabasePluginMessages::ResourceType>(request.level() + 2));
           Orthanc::DatabasePluginMessages::Find_Response_MultipleMetadata* metadata = content->add_metadata();
 
-          metadata->add_values(statement.ReadString(C3_STRING_1));
-          metadata->set_key(statement.ReadInteger32(C6_INT_1));
+          metadata->add_values(statement->ReadString(C3_STRING_1));
+          metadata->set_key(statement->ReadInteger32(C6_INT_1));
         }; break;
 
         case QUERY_GRAND_GRAND_CHILDREN_IDENTIFIERS:
         {
           Orthanc::DatabasePluginMessages::Find_Response_ChildrenContent* content = GetChildrenContent(responses[internalId], static_cast<Orthanc::DatabasePluginMessages::ResourceType>(request.level() + 3));
-          content->add_identifiers(statement.ReadString(C3_STRING_1));
+          content->add_identifiers(statement->ReadString(C3_STRING_1));
         }; break;
 
         case QUERY_ATTACHMENTS:
         {
           Orthanc::DatabasePluginMessages::FileInfo* attachment = responses[internalId]->add_attachments();
 
-          attachment->set_uuid(statement.ReadString(C3_STRING_1));
-          attachment->set_uncompressed_hash(statement.ReadString(C4_STRING_2));
-          attachment->set_compressed_hash(statement.ReadString(C5_STRING_3));
-          attachment->set_content_type(statement.ReadInteger32(C6_INT_1));
-          attachment->set_compression_type(statement.ReadInteger32(C7_INT_2));
-          attachment->set_compressed_size(statement.ReadInteger64(C8_BIG_INT_1));
-          attachment->set_uncompressed_size(statement.ReadInteger64(C9_BIG_INT_2));
+          attachment->set_uuid(statement->ReadString(C3_STRING_1));
+          attachment->set_uncompressed_hash(statement->ReadString(C4_STRING_2));
+          attachment->set_compressed_hash(statement->ReadString(C5_STRING_3));
+          attachment->set_content_type(statement->ReadInteger32(C6_INT_1));
+          attachment->set_compression_type(statement->ReadInteger32(C7_INT_2));
+          attachment->set_compressed_size(statement->ReadInteger64(C8_BIG_INT_1));
+          attachment->set_uncompressed_size(statement->ReadInteger64(C9_BIG_INT_2));
         }; break;
 
         case QUERY_METADATA:
@@ -3831,8 +3881,8 @@
           Orthanc::DatabasePluginMessages::Find_Response_ResourceContent* content = GetResourceContent(responses[internalId], request.level());
           Orthanc::DatabasePluginMessages::Find_Response_Metadata* metadata = content->add_metadata();
 
-          metadata->set_value(statement.ReadString(C3_STRING_1));
-          metadata->set_key(statement.ReadInteger32(C6_INT_1));
+          metadata->set_value(statement->ReadString(C3_STRING_1));
+          metadata->set_key(statement->ReadInteger32(C6_INT_1));
         }; break;
 
         case QUERY_PARENT_METADATA:
@@ -3840,8 +3890,8 @@
           Orthanc::DatabasePluginMessages::Find_Response_ResourceContent* content = GetResourceContent(responses[internalId], static_cast<Orthanc::DatabasePluginMessages::ResourceType>(request.level() - 1));
           Orthanc::DatabasePluginMessages::Find_Response_Metadata* metadata = content->add_metadata();
 
-          metadata->set_value(statement.ReadString(C3_STRING_1));
-          metadata->set_key(statement.ReadInteger32(C6_INT_1));
+          metadata->set_value(statement->ReadString(C3_STRING_1));
+          metadata->set_key(statement->ReadInteger32(C6_INT_1));
         }; break;
 
         case QUERY_GRAND_PARENT_METADATA:
@@ -3849,43 +3899,43 @@
           Orthanc::DatabasePluginMessages::Find_Response_ResourceContent* content = GetResourceContent(responses[internalId], static_cast<Orthanc::DatabasePluginMessages::ResourceType>(request.level() - 2));
           Orthanc::DatabasePluginMessages::Find_Response_Metadata* metadata = content->add_metadata();
 
-          metadata->set_value(statement.ReadString(C3_STRING_1));
-          metadata->set_key(statement.ReadInteger32(C6_INT_1));
+          metadata->set_value(statement->ReadString(C3_STRING_1));
+          metadata->set_key(statement->ReadInteger32(C6_INT_1));
         }; break;
 
         case QUERY_PARENT_IDENTIFIER:
         {
-          responses[internalId]->set_parent_public_id(statement.ReadString(C3_STRING_1));
+          responses[internalId]->set_parent_public_id(statement->ReadString(C3_STRING_1));
         }; break;
 
         case QUERY_ONE_INSTANCE_IDENTIFIER:
         {
-          responses[internalId]->set_one_instance_public_id(statement.ReadString(C3_STRING_1));
+          responses[internalId]->set_one_instance_public_id(statement->ReadString(C3_STRING_1));
         }; break;
         case QUERY_ONE_INSTANCE_METADATA:
         {
           Orthanc::DatabasePluginMessages::Find_Response_Metadata* metadata = responses[internalId]->add_one_instance_metadata();
 
-          metadata->set_value(statement.ReadString(C3_STRING_1));
-          metadata->set_key(statement.ReadInteger32(C6_INT_1));
+          metadata->set_value(statement->ReadString(C3_STRING_1));
+          metadata->set_key(statement->ReadInteger32(C6_INT_1));
         }; break;
         case QUERY_ONE_INSTANCE_ATTACHMENTS:
         {
           Orthanc::DatabasePluginMessages::FileInfo* attachment = responses[internalId]->add_one_instance_attachments();
           
-          attachment->set_uuid(statement.ReadString(C3_STRING_1));
-          attachment->set_uncompressed_hash(statement.ReadString(C4_STRING_2));
-          attachment->set_compressed_hash(statement.ReadString(C5_STRING_3));
-          attachment->set_content_type(statement.ReadInteger32(C6_INT_1));
-          attachment->set_compression_type(statement.ReadInteger32(C7_INT_2));
-          attachment->set_compressed_size(statement.ReadInteger64(C8_BIG_INT_1));
-          attachment->set_uncompressed_size(statement.ReadInteger64(C9_BIG_INT_2));
+          attachment->set_uuid(statement->ReadString(C3_STRING_1));
+          attachment->set_uncompressed_hash(statement->ReadString(C4_STRING_2));
+          attachment->set_compressed_hash(statement->ReadString(C5_STRING_3));
+          attachment->set_content_type(statement->ReadInteger32(C6_INT_1));
+          attachment->set_compression_type(statement->ReadInteger32(C7_INT_2));
+          attachment->set_compressed_size(statement->ReadInteger64(C8_BIG_INT_1));
+          attachment->set_uncompressed_size(statement->ReadInteger64(C9_BIG_INT_2));
         }; break;
 
         default:
           throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
       }
-      statement.Next();
+      statement->Next();
     }    
   }
 #endif
--- a/MySQL/NEWS	Tue Oct 15 15:52:39 2024 +0200
+++ b/MySQL/NEWS	Mon Oct 21 14:34:57 2024 +0200
@@ -3,6 +3,7 @@
 
 * Added support for ExtendedChanges:
   - changes?type=...&to=...
+* Added support for ExtendedFind
 * Fixed a memory leak when executing non cached SQL statements (rarely used)
 
 
--- a/MySQL/Plugins/MySQLIndex.cpp	Tue Oct 15 15:52:39 2024 +0200
+++ b/MySQL/Plugins/MySQLIndex.cpp	Mon Oct 21 14:34:57 2024 +0200
@@ -599,26 +599,26 @@
   bool MySQLIndex::HasFindSupport() const
   {
     // TODO-FIND
-    return false;
+    return true;
   }
 #endif
 
 
 #if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 5)
-  void MySQLIndex::ExecuteFind(Orthanc::DatabasePluginMessages::TransactionResponse& response,
-                               DatabaseManager& manager,
-                               const Orthanc::DatabasePluginMessages::Find_Request& request)
-  {
-    // TODO-FIND
-    throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
-  }
+  // void MySQLIndex::ExecuteFind(Orthanc::DatabasePluginMessages::TransactionResponse& response,
+  //                              DatabaseManager& manager,
+  //                              const Orthanc::DatabasePluginMessages::Find_Request& request)
+  // {
+  //   // TODO-FIND
+  //   throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+  // }
 
-  void MySQLIndex::ExecuteCount(Orthanc::DatabasePluginMessages::TransactionResponse& response,
-                                DatabaseManager& manager,
-                                const Orthanc::DatabasePluginMessages::Find_Request& request)
-  {
-    // TODO-FIND
-    throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
-  }
+  // void MySQLIndex::ExecuteCount(Orthanc::DatabasePluginMessages::TransactionResponse& response,
+  //                               DatabaseManager& manager,
+  //                               const Orthanc::DatabasePluginMessages::Find_Request& request)
+  // {
+  //   // TODO-FIND
+  //   throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+  // }
 #endif
 }
--- a/MySQL/Plugins/MySQLIndex.h	Tue Oct 15 15:52:39 2024 +0200
+++ b/MySQL/Plugins/MySQLIndex.h	Mon Oct 21 14:34:57 2024 +0200
@@ -91,13 +91,13 @@
 #endif
 
 #if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 5)
-    virtual void ExecuteFind(Orthanc::DatabasePluginMessages::TransactionResponse& response,
-                             DatabaseManager& manager,
-                             const Orthanc::DatabasePluginMessages::Find_Request& request) ORTHANC_OVERRIDE;
+    // virtual void ExecuteFind(Orthanc::DatabasePluginMessages::TransactionResponse& response,
+    //                          DatabaseManager& manager,
+    //                          const Orthanc::DatabasePluginMessages::Find_Request& request) ORTHANC_OVERRIDE;
 
-    virtual void ExecuteCount(Orthanc::DatabasePluginMessages::TransactionResponse& response,
-                              DatabaseManager& manager,
-                              const Orthanc::DatabasePluginMessages::Find_Request& request) ORTHANC_OVERRIDE;
+    // virtual void ExecuteCount(Orthanc::DatabasePluginMessages::TransactionResponse& response,
+    //                           DatabaseManager& manager,
+    //                           const Orthanc::DatabasePluginMessages::Find_Request& request) ORTHANC_OVERRIDE;
 #endif
   };
 }