changeset 3027:fd587cf51a89 db-changes

cont
author Sebastien Jodogne <s.jodogne@gmail.com>
date Tue, 18 Dec 2018 12:50:27 +0100
parents 039a9d262d64
children ea653ec47f31
files Core/Enumerations.cpp Core/Enumerations.h OrthancServer/IDatabaseWrapper.h OrthancServer/OrthancRestApi/OrthancRestResources.cpp OrthancServer/SQLiteDatabaseWrapper.cpp OrthancServer/SQLiteDatabaseWrapper.h OrthancServer/Search/DatabaseLookup.cpp OrthancServer/Search/DatabaseLookup.h OrthancServer/Search/DicomTagConstraint.cpp OrthancServer/Search/DicomTagConstraint.h OrthancServer/ServerContext.cpp OrthancServer/ServerIndex.cpp OrthancServer/ServerIndex.h Plugins/Engine/OrthancPluginDatabase.cpp Plugins/Engine/OrthancPluginDatabase.h UnitTestsSources/DatabaseLookupTests.cpp UnitTestsSources/UnitTestsMain.cpp
diffstat 17 files changed, 538 insertions(+), 279 deletions(-) [+]
line wrap: on
line diff
--- a/Core/Enumerations.cpp	Mon Dec 17 17:05:28 2018 +0100
+++ b/Core/Enumerations.cpp	Tue Dec 18 12:50:27 2018 +0100
@@ -1897,6 +1897,35 @@
   }
 
 
+  bool IsResourceLevelAboveOrEqual(ResourceType level,
+                                   ResourceType reference)
+  {
+    switch (reference)
+    {
+      case ResourceType_Patient:
+        return (level == ResourceType_Patient);
+
+      case ResourceType_Study:
+        return (level == ResourceType_Patient ||
+                level == ResourceType_Study);
+
+      case ResourceType_Series:
+        return (level == ResourceType_Patient ||
+                level == ResourceType_Study ||
+                level == ResourceType_Series);
+
+      case ResourceType_Instance:
+        return (level == ResourceType_Patient ||
+                level == ResourceType_Study ||
+                level == ResourceType_Series ||
+                level == ResourceType_Instance);
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
   DicomModule GetModule(ResourceType type)
   {
     switch (type)
--- a/Core/Enumerations.h	Mon Dec 17 17:05:28 2018 +0100
+++ b/Core/Enumerations.h	Tue Dec 18 12:50:27 2018 +0100
@@ -758,6 +758,9 @@
 
   ResourceType GetParentResourceType(ResourceType type);
 
+  bool IsResourceLevelAboveOrEqual(ResourceType level,
+                                   ResourceType reference);
+
   DicomModule GetModule(ResourceType type);
 
   const char* GetDicomSpecificCharacterSet(Encoding encoding);
--- a/OrthancServer/IDatabaseWrapper.h	Mon Dec 17 17:05:28 2018 +0100
+++ b/OrthancServer/IDatabaseWrapper.h	Tue Dec 18 12:50:27 2018 +0100
@@ -226,15 +226,13 @@
 
     virtual bool IsDiskSizeAbove(uint64_t threshold) = 0;
 
-    virtual void FindOneChildInstance(std::vector<std::string>& instancesId,
-                                      const std::vector<std::string>& resourcesId,
-                                      ResourceType level) = 0;
-
     virtual void ApplyLookupPatients(std::vector<std::string>& patientsId,
+                                     std::vector<std::string>& instancesId,
                                      const DatabaseLookup& lookup,
                                      size_t limit) = 0;
 
     virtual void ApplyLookupResources(std::vector<std::string>& resourcesId,
+                                      std::vector<std::string>& instancesId,
                                       const DatabaseLookup& lookup,
                                       ResourceType queryLevel,
                                       size_t limit) = 0;
--- a/OrthancServer/OrthancRestApi/OrthancRestResources.cpp	Mon Dec 17 17:05:28 2018 +0100
+++ b/OrthancServer/OrthancRestApi/OrthancRestResources.cpp	Tue Dec 18 12:50:27 2018 +0100
@@ -1418,9 +1418,9 @@
         query.AddDicomConstraint(FromDcmtkBridge::ParseTag(members[i]), 
                                  request[KEY_QUERY][members[i]].asString(),
                                  caseSensitive);
-        query2.AddDicomConstraint(FromDcmtkBridge::ParseTag(members[i]), 
-                                  request[KEY_QUERY][members[i]].asString(),
-                                  caseSensitive, true);
+        query2.AddRestConstraint(FromDcmtkBridge::ParseTag(members[i]), 
+                                 request[KEY_QUERY][members[i]].asString(),
+                                 caseSensitive, true);
       }
 
       FindVisitor visitor;
--- a/OrthancServer/SQLiteDatabaseWrapper.cpp	Mon Dec 17 17:05:28 2018 +0100
+++ b/OrthancServer/SQLiteDatabaseWrapper.cpp	Tue Dec 18 12:50:27 2018 +0100
@@ -339,6 +339,9 @@
     db_.Execute("PRAGMA WAL_AUTOCHECKPOINT=1000;");
     //db_.Execute("PRAGMA TEMP_STORE=memory");
 
+    // Make "LIKE" case-sensitive in SQLite 
+    db_.Execute("PRAGMA case_sensitive_like = true;");
+    
     if (!db_.DoesTableExist("GlobalProperties"))
     {
       LOG(INFO) << "Creating the database";
@@ -1203,129 +1206,19 @@
   }
 
 
-  namespace
-  {
-    class MainDicomTagsRegistry : public boost::noncopyable
-    {
-    private:
-      class TagInfo
-      {
-      private:
-        ResourceType  level_;
-        DicomTagType  type_;
-
-      public:
-        TagInfo()
-        {
-        }
-
-        TagInfo(ResourceType level,
-                DicomTagType type) :
-          level_(level),
-          type_(type)
-        {
-        }
-
-        ResourceType GetLevel() const
-        {
-          return level_;
-        }
-
-        DicomTagType GetType() const
-        {
-          return type_;
-        }
-      };
-      
-      typedef std::map<DicomTag, TagInfo>   Registry;
-
-
-      Registry  registry_;
-      
-      void LoadTags(ResourceType level)
-      {
-        const DicomTag* tags = NULL;
-        size_t size;
-  
-        ServerToolbox::LoadIdentifiers(tags, size, level);
-  
-        for (size_t i = 0; i < size; i++)
-        {
-          if (registry_.find(tags[i]) == registry_.end())
-          {
-            registry_[tags[i]] = TagInfo(level, DicomTagType_Identifier);
-          }
-          else
-          {
-            // These patient-level tags are copied in the study level
-            assert(level == ResourceType_Study &&
-                   (tags[i] == DICOM_TAG_PATIENT_ID ||
-                    tags[i] == DICOM_TAG_PATIENT_NAME ||
-                    tags[i] == DICOM_TAG_PATIENT_BIRTH_DATE));
-          }
-        }
-  
-        DicomMap::LoadMainDicomTags(tags, size, level);
-  
-        for (size_t i = 0; i < size; i++)
-        {
-          if (registry_.find(tags[i]) == registry_.end())
-          {
-            registry_[tags[i]] = TagInfo(level, DicomTagType_Main);
-          }
-        }
-      }
-
-    public:
-      MainDicomTagsRegistry()
-      {
-        LoadTags(ResourceType_Patient);
-        LoadTags(ResourceType_Study);
-        LoadTags(ResourceType_Series);
-        LoadTags(ResourceType_Instance); 
-      }
-
-      void LookupTag(ResourceType& level,
-                     DicomTagType& type,
-                     const DicomTag& tag) const
-      {
-        Registry::const_iterator it = registry_.find(tag);
-
-        if (it == registry_.end())
-        {
-          // Default values
-          level = ResourceType_Instance;
-          type = DicomTagType_Generic;
-        }
-        else
-        {
-          level = it->second.GetLevel();
-          type = it->second.GetType();
-        }
-      }
-    };
-  }
-  
-
-  void SQLiteDatabaseWrapper::FindOneChildInstance(std::vector<std::string>& instancesId,
-                                                   const std::vector<std::string>& resourcesId,
-                                                   ResourceType level)
-  {
-    printf("ICI 3\n");
-
-    throw OrthancException(ErrorCode_NotImplemented);
-  }
-
-
   void SQLiteDatabaseWrapper::ApplyLookupPatients(std::vector<std::string>& patientsId,
+                                                  std::vector<std::string>& instancesId,
                                                   const DatabaseLookup& lookup,
                                                   size_t limit)
   {
-    static const MainDicomTagsRegistry registry;
-    
     printf("ICI 1\n");
 
-    std::string heading = "SELECT patient.publicId FROM Resources AS patient ";
+    {
+      SQLite::Statement s(db_, "DROP TABLE IF EXISTS Lookup");
+      s.Run();
+    }
+    
+    std::string heading = "CREATE TEMPORARY TABLE Lookup AS SELECT patient.publicId, patient.internalId FROM Resources AS patient ";
     std::string trailer;
     std::vector<std::string> parameters;
 
@@ -1333,75 +1226,114 @@
     {
       const DicomTagConstraint& constraint = lookup.GetConstraint(i);
       
-      ResourceType level;
-      DicomTagType type;
-      registry.LookupTag(level, type, constraint.GetTag());
-
-      if (level != ResourceType_Patient)
+      if (constraint.GetTagType() != DicomTagType_Identifier &&
+          constraint.GetTagType() != DicomTagType_Main)
       {
-        throw OrthancException(ErrorCode_ParameterOutOfRange,
-                               "Not a patient-level tag: (" +
-                               lookup.GetConstraint(i).GetTag().Format() + ")");
+        // This should have been set by ServerIndex::NormalizeLookup()"
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
       }
+      
+      std::string tag = "t" + boost::lexical_cast<std::string>(i);
 
-      if (type == DicomTagType_Identifier ||
-          type == DicomTagType_Main)
+      char on[128];
+      sprintf(
+        on, "%s JOIN %s %s ON %s.id = patient.internalId AND %s.tagGroup = 0x%04x AND %s.tagElement = 0x%04x ",
+        constraint.IsMandatory() ? "INNER" : "LEFT",
+        constraint.GetTagType() == DicomTagType_Identifier ? "DicomIdentifiers" : "MainDicomTags",
+        tag.c_str(), tag.c_str(), tag.c_str(), constraint.GetTag().GetGroup(),
+        tag.c_str(), constraint.GetTag().GetElement());
+
+      std::string comparison;
+        
+      switch (constraint.GetConstraintType())
       {
-        std::string table = (type == DicomTagType_Identifier ? "DicomIdentifiers" : "MainDicomTags");
-        std::string tag = "t" + boost::lexical_cast<std::string>(i);
+        case ConstraintType_Equal:
+        case ConstraintType_SmallerOrEqual:
+        case ConstraintType_GreaterOrEqual:
+        {
+          std::string op;
+          switch (constraint.GetConstraintType())
+          {
+            case ConstraintType_Equal:
+              op = "=";
+              break;
+          
+            case ConstraintType_SmallerOrEqual:
+              op = "<=";
+              break;
+          
+            case ConstraintType_GreaterOrEqual:
+              op = ">=";
+              break;
+          
+            default:
+              throw OrthancException(ErrorCode_InternalError);
+          }
+          
+          parameters.push_back(constraint.GetValue());
 
-        char on[128];
-        sprintf(on, " %s ON %s.id = patient.internalId AND %s.tagGroup = 0x%04x AND %s.tagElement = 0x%04x ",
-                tag.c_str(), tag.c_str(), tag.c_str(), constraint.GetTag().GetGroup(),
-                tag.c_str(), constraint.GetTag().GetElement());
-        
-        if (constraint.IsMandatory())
-        {
-          heading += "INNER JOIN " + table + std::string(on);
-        }
-        else
-        {
-          heading += "LEFT JOIN " + table + std::string(on);
+          if (constraint.IsCaseSensitive())
+          {
+            comparison = tag + ".value " + op + " ?";
+          }
+          else
+          {
+            comparison = "lower(" + tag + ".value) " + op + " lower(?)";
+          }
+
+          break;
         }
 
-        trailer += "AND (";
-
-        if (!constraint.IsMandatory())
-        {
-          trailer += tag + ".value IS NULL OR ";
-        }
+        case ConstraintType_List:
+          for (std::set<std::string>::const_iterator
+                 it = constraint.GetValues().begin();
+               it != constraint.GetValues().end(); ++it)
+          {
+            parameters.push_back(*it);
 
-        if (constraint.IsCaseSensitive())
-        {
-          trailer += tag + ".value ";
-        }
-        else
-        {
-          trailer += "lower(" + tag + ".value) ";
-        }
-
-        switch (constraint.GetType())
-        {
-          case ConstraintType_Equal:
-            parameters.push_back(constraint.GetValue());
+            if (!comparison.empty())
+            {
+              comparison += ", ";
+            }
             
             if (constraint.IsCaseSensitive())
             {
-              trailer += "= ?";
+              comparison += "?";
             }
             else
             {
-              trailer += "= lower(?)";
+              comparison += "lower(?)";
             }
+          }
 
-            break;
+          if (constraint.IsCaseSensitive())
+          {
+            comparison = tag + ".value IN (" + comparison + ")";
+          }
+          else
+          {
+            comparison = "lower(" +  tag + ".value) IN (" + comparison + ")";
+          }
+            
+          break;
 
-          default:
-            throw OrthancException(ErrorCode_NotImplemented);
-        }
+          //case ConstraintType_Wildcard:
+          //break;
+
+        default:
+          continue;
+      }
+
+      heading += std::string(on);
 
-        trailer += ") ";
+      trailer += "AND (";
+
+      if (!constraint.IsMandatory())
+      {
+        trailer += tag + ".value IS NULL OR ";
       }
+
+      trailer += comparison + ") ";
     }
 
     if (limit != 0)
@@ -1409,26 +1341,41 @@
       trailer += " LIMIT " + boost::lexical_cast<std::string>(limit);
     }
 
-    std::string sql = (heading + "WHERE patient.resourceType = " +
-                       boost::lexical_cast<std::string>(ResourceType_Patient) + " " + trailer);
+    {
+      std::string sql = (heading + "WHERE patient.resourceType = " +
+                         boost::lexical_cast<std::string>(ResourceType_Patient) + " " + trailer);
 
-    SQLite::Statement s(db_, sql);
+      printf("[%s]\n", sql.c_str());
+
+      SQLite::Statement s(db_, sql);
 
-    printf("[%s]\n", sql.c_str());
+      for (size_t i = 0; i < parameters.size(); i++)
+      {
+        printf("   %lu = '%s'\n", i, parameters[i].c_str());
+        s.BindString(i, parameters[i]);
+      }
 
-    for (size_t i = 0; i < parameters.size(); i++)
-    {
-      printf("   %d = '%s'\n", i, parameters[i].c_str());
-      s.BindString(i, parameters[i]);
+      s.Run();
     }
 
-    patientsId.clear();
+    {
+      SQLite::Statement s
+        (db_, "SELECT patient.publicId, instances.publicID FROM Lookup AS patient "
+         "INNER JOIN Resources studies ON patient.internalId=studies.parentId "
+         "INNER JOIN Resources series ON studies.internalId=series.parentId "
+         "INNER JOIN Resources instances ON series.internalId=instances.parentId "
+         "GROUP BY patient.publicId");
+      
+      patientsId.clear();
 
-    while (s.Step())
-    {
-      std::string publicId = s.ColumnString(0);
-      patientsId.push_back(publicId);
-      printf("** [%s]\n", publicId.c_str());
+      while (s.Step())
+      {
+        const std::string patient = s.ColumnString(0);
+        const std::string instance = s.ColumnString(1);
+        patientsId.push_back(patient);
+        instancesId.push_back(instance);
+        printf("** [%s] [%s]\n", patient.c_str(), instance.c_str());
+      }
     }
     
     throw OrthancException(ErrorCode_NotImplemented);
@@ -1436,6 +1383,7 @@
   
 
   void SQLiteDatabaseWrapper::ApplyLookupResources(std::vector<std::string>& resourcesId,
+                                                   std::vector<std::string>& instancesId,
                                                    const DatabaseLookup& lookup,
                                                    ResourceType queryLevel,
                                                    size_t limit)
--- a/OrthancServer/SQLiteDatabaseWrapper.h	Mon Dec 17 17:05:28 2018 +0100
+++ b/OrthancServer/SQLiteDatabaseWrapper.h	Tue Dec 18 12:50:27 2018 +0100
@@ -277,15 +277,13 @@
 
     virtual bool IsDiskSizeAbove(uint64_t threshold);
 
-    virtual void FindOneChildInstance(std::vector<std::string>& instancesId,
-                                      const std::vector<std::string>& resourcesId,
-                                      ResourceType level);
-
     virtual void ApplyLookupPatients(std::vector<std::string>& patientsId,
+                                     std::vector<std::string>& instancesId,
                                      const DatabaseLookup& lookup,
                                      size_t limit);
 
     virtual void ApplyLookupResources(std::vector<std::string>& resourcesId,
+                                      std::vector<std::string>& instancesId,
                                       const DatabaseLookup& lookup,
                                       ResourceType queryLevel,
                                       size_t limit);
--- a/OrthancServer/Search/DatabaseLookup.cpp	Mon Dec 17 17:05:28 2018 +0100
+++ b/OrthancServer/Search/DatabaseLookup.cpp	Tue Dec 18 12:50:27 2018 +0100
@@ -91,55 +91,12 @@
   }
 
 
-  void DatabaseLookup::AddDicomConstraint(const DicomTag& tag,
-                                          const std::string& dicomQuery,
-                                          bool caseSensitivePN,
-                                          bool mandatoryTag)
+  void DatabaseLookup::AddDicomConstraintInternal(const DicomTag& tag,
+                                                  ValueRepresentation vr,
+                                                  const std::string& dicomQuery,
+                                                  bool caseSensitive,
+                                                  bool mandatoryTag)
   {
-    ValueRepresentation vr = FromDcmtkBridge::LookupValueRepresentation(tag);
-
-    if (vr == ValueRepresentation_Sequence)
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-
-    /**
-     * DICOM specifies that searches must always be case sensitive,
-     * except for tags with a PN value representation. For PN, Orthanc
-     * uses the configuration option "CaseSensitivePN" to decide
-     * whether matching is case-sensitive or case-insensitive.
-     *
-     * Reference: DICOM PS 3.4
-     *   - C.2.2.2.1 ("Single Value Matching") 
-     *   - C.2.2.2.4 ("Wild Card Matching")
-     * http://medical.nema.org/Dicom/2011/11_04pu.pdf
-     *
-     * "Except for Attributes with a PN Value Representation, only
-     * entities with values which match exactly the value specified in the
-     * request shall match. This matching is case-sensitive, i.e.,
-     * sensitive to the exact encoding of the key attribute value in
-     * character sets where a letter may have multiple encodings (e.g.,
-     * based on its case, its position in a word, or whether it is
-     * accented)
-     * 
-     * For Attributes with a PN Value Representation (e.g., Patient Name
-     * (0010,0010)), an application may perform literal matching that is
-     * either case-sensitive, or that is insensitive to some or all
-     * aspects of case, position, accent, or other character encoding
-     * variants."
-     *
-     * (0008,0018) UI SOPInstanceUID     => Case-sensitive
-     * (0008,0050) SH AccessionNumber    => Case-sensitive
-     * (0010,0020) LO PatientID          => Case-sensitive
-     * (0020,000D) UI StudyInstanceUID   => Case-sensitive
-     * (0020,000E) UI SeriesInstanceUID  => Case-sensitive
-     **/
-    bool caseSensitive = true;
-    if (vr == ValueRepresentation_PersonName)
-    {
-      caseSensitive = caseSensitivePN;
-    }
-
     if ((vr == ValueRepresentation_Date ||
          vr == ValueRepresentation_DateTime ||
          vr == ValueRepresentation_Time) &&
@@ -205,4 +162,69 @@
                     (tag, ConstraintType_Equal, dicomQuery, caseSensitive, mandatoryTag));
     }
   }
+
+
+  void DatabaseLookup::AddDicomConstraint(const DicomTag& tag,
+                                          const std::string& dicomQuery,
+                                          bool caseSensitivePN,
+                                          bool mandatoryTag)
+  {
+    ValueRepresentation vr = FromDcmtkBridge::LookupValueRepresentation(tag);
+
+    if (vr == ValueRepresentation_Sequence)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    /**
+     * DICOM specifies that searches must always be case sensitive,
+     * except for tags with a PN value representation. For PN, Orthanc
+     * uses the configuration option "CaseSensitivePN" to decide
+     * whether matching is case-sensitive or case-insensitive.
+     *
+     * Reference: DICOM PS 3.4
+     *   - C.2.2.2.1 ("Single Value Matching") 
+     *   - C.2.2.2.4 ("Wild Card Matching")
+     * http://medical.nema.org/Dicom/2011/11_04pu.pdf
+     *
+     * "Except for Attributes with a PN Value Representation, only
+     * entities with values which match exactly the value specified in the
+     * request shall match. This matching is case-sensitive, i.e.,
+     * sensitive to the exact encoding of the key attribute value in
+     * character sets where a letter may have multiple encodings (e.g.,
+     * based on its case, its position in a word, or whether it is
+     * accented)
+     * 
+     * For Attributes with a PN Value Representation (e.g., Patient Name
+     * (0010,0010)), an application may perform literal matching that is
+     * either case-sensitive, or that is insensitive to some or all
+     * aspects of case, position, accent, or other character encoding
+     * variants."
+     *
+     * (0008,0018) UI SOPInstanceUID     => Case-sensitive
+     * (0008,0050) SH AccessionNumber    => Case-sensitive
+     * (0010,0020) LO PatientID          => Case-sensitive
+     * (0020,000D) UI StudyInstanceUID   => Case-sensitive
+     * (0020,000E) UI SeriesInstanceUID  => Case-sensitive
+     **/
+    
+    if (vr == ValueRepresentation_PersonName)
+    {
+      AddDicomConstraintInternal(tag, vr, dicomQuery, caseSensitivePN, mandatoryTag);
+    }
+    else
+    {
+      AddDicomConstraintInternal(tag, vr, dicomQuery, true /* case sensitive */, mandatoryTag);
+    }
+  }
+
+
+  void DatabaseLookup::AddRestConstraint(const DicomTag& tag,
+                                         const std::string& dicomQuery,
+                                         bool caseSensitive,
+                                         bool mandatoryTag)
+  {
+    AddDicomConstraintInternal(tag, FromDcmtkBridge::LookupValueRepresentation(tag),
+                               dicomQuery, caseSensitive, mandatoryTag);
+  }
 }
--- a/OrthancServer/Search/DatabaseLookup.h	Mon Dec 17 17:05:28 2018 +0100
+++ b/OrthancServer/Search/DatabaseLookup.h	Tue Dec 18 12:50:27 2018 +0100
@@ -42,6 +42,11 @@
   private:
     std::vector<DicomTagConstraint*>  constraints_;
 
+    void AddDicomConstraintInternal(const DicomTag& tag,
+                                    ValueRepresentation vr,
+                                    const std::string& dicomQuery,
+                                    bool caseSensitive,
+                                    bool mandatoryTag);
   public:
     DatabaseLookup()
     {
@@ -69,5 +74,10 @@
                             const std::string& dicomQuery,
                             bool caseSensitivePN,
                             bool mandatoryTag);
+
+    void AddRestConstraint(const DicomTag& tag,
+                           const std::string& dicomQuery,
+                           bool caseSensitive,
+                           bool mandatoryTag);
   };
 }
--- a/OrthancServer/Search/DicomTagConstraint.cpp	Mon Dec 17 17:05:28 2018 +0100
+++ b/OrthancServer/Search/DicomTagConstraint.cpp	Tue Dec 18 12:50:27 2018 +0100
@@ -100,6 +100,7 @@
                                          bool caseSensitive,
                                          bool mandatory) :
     tag_(tag),
+    tagType_(DicomTagType_Generic),
     constraintType_(type),
     caseSensitive_(caseSensitive),
     mandatory_(mandatory)
@@ -130,6 +131,7 @@
                                          bool caseSensitive,
                                          bool mandatory) :
     tag_(tag),
+    tagType_(DicomTagType_Generic),
     constraintType_(type),
     caseSensitive_(caseSensitive),
     mandatory_(mandatory)
@@ -141,6 +143,20 @@
   }
 
 
+  DicomTagConstraint* DicomTagConstraint::Clone() const
+  {
+    std::auto_ptr<DicomTagConstraint> clone(new DicomTagConstraint);
+    
+    clone->tag_ = tag_;
+    clone->constraintType_ = constraintType_;
+    clone->values_ = values_;
+    clone->caseSensitive_ = caseSensitive_;
+    clone->mandatory_ = mandatory_;
+
+    return clone.release();
+  }
+  
+
   void DicomTagConstraint::AddValue(const std::string& value)
   {
     if (constraintType_ != ConstraintType_List)
--- a/OrthancServer/Search/DicomTagConstraint.h	Mon Dec 17 17:05:28 2018 +0100
+++ b/OrthancServer/Search/DicomTagConstraint.h	Tue Dec 18 12:50:27 2018 +0100
@@ -47,6 +47,7 @@
     class RegularExpression;
 
     DicomTag                tag_;
+    DicomTagType            tagType_;
     ConstraintType          constraintType_;
     std::set<std::string>   values_;
     bool                    caseSensitive_;
@@ -54,6 +55,12 @@
 
     boost::shared_ptr<RegularExpression>  regex_;
 
+    DicomTagConstraint() :
+      tag_(0, 0),
+      tagType_(DicomTagType_Generic)
+    {
+    }
+    
   public:
     DicomTagConstraint(const DicomTag& tag,
                        ConstraintType type,
@@ -67,12 +74,25 @@
                        bool caseSensitive,
                        bool mandatory);
 
+    DicomTagConstraint* Clone() const;    
+
     const DicomTag& GetTag() const
     {
       return tag_;
     }
 
-    ConstraintType GetType() const
+    DicomTagType GetTagType() const
+    {
+      return tagType_;
+    }
+
+    // Set by "ServerIndex::NormalizeLookup()"
+    void SetTagType(DicomTagType type)
+    {
+      tagType_ = type;
+    }
+    
+    ConstraintType GetConstraintType() const
     {
       return constraintType_;
     }
@@ -82,6 +102,11 @@
       return caseSensitive_;
     }
 
+    void SetCaseSensitive(bool caseSensitive)
+    {
+      caseSensitive_ = caseSensitive;
+    }
+
     bool IsMandatory() const
     {
       return mandatory_;
--- a/OrthancServer/ServerContext.cpp	Mon Dec 17 17:05:28 2018 +0100
+++ b/OrthancServer/ServerContext.cpp	Tue Dec 18 12:50:27 2018 +0100
@@ -811,19 +811,31 @@
     std::vector<std::string> resources, instances;
     GetIndex().FindCandidates(resources, instances, lookup);
 
+    bool complete = true;
+
 #if 1
     {
       std::vector<std::string> resources2, instances2;
 
+      size_t lookupLimit = (limit == 0 ? 0 : limit + 1);
+      
       if (lookup.GetLevel() == ResourceType_Patient)
       {
-        GetIndex().ApplyLookupPatients(resources2, instances2, lookup2, limit /* TODO */);
+        GetIndex().ApplyLookupPatients(resources2, instances2, lookup2, lookupLimit);
       }
       else
       {
-        GetIndex().ApplyLookupResources(resources2, instances2, lookup2, lookup.GetLevel(), limit /* TODO */);
+        GetIndex().ApplyLookupResources(resources2, instances2, lookup2,
+                                        lookup.GetLevel(), lookupLimit);
       }
 
+      if (limit != 0 &&
+          resources2.size() > limit)
+      {
+        complete = false;
+      }
+      
+      // Sanity checks
       std::set<std::string> r;
       for (size_t i = 0; i < resources2.size(); i++)
       {
@@ -845,7 +857,6 @@
 
     size_t countResults = 0;
     size_t skipped = 0;
-    bool complete = true;
 
     const bool isDicomAsJsonNeeded = visitor.IsDicomAsJsonNeeded();
     
--- a/OrthancServer/ServerIndex.cpp	Mon Dec 17 17:05:28 2018 +0100
+++ b/OrthancServer/ServerIndex.cpp	Tue Dec 18 12:50:27 2018 +0100
@@ -299,6 +299,107 @@
   };
 
 
+  class ServerIndex::MainDicomTagsRegistry : public boost::noncopyable
+  {
+  private:
+    class TagInfo
+    {
+    private:
+      ResourceType  level_;
+      DicomTagType  type_;
+
+    public:
+      TagInfo()
+      {
+      }
+
+      TagInfo(ResourceType level,
+              DicomTagType type) :
+        level_(level),
+        type_(type)
+      {
+      }
+
+      ResourceType GetLevel() const
+      {
+        return level_;
+      }
+
+      DicomTagType GetType() const
+      {
+        return type_;
+      }
+    };
+      
+    typedef std::map<DicomTag, TagInfo>   Registry;
+
+
+    Registry  registry_;
+      
+    void LoadTags(ResourceType level)
+    {
+      const DicomTag* tags = NULL;
+      size_t size;
+  
+      ServerToolbox::LoadIdentifiers(tags, size, level);
+  
+      for (size_t i = 0; i < size; i++)
+      {
+        if (registry_.find(tags[i]) == registry_.end())
+        {
+          registry_[tags[i]] = TagInfo(level, DicomTagType_Identifier);
+        }
+        else
+        {
+          // These patient-level tags are copied in the study level
+          assert(level == ResourceType_Study &&
+                 (tags[i] == DICOM_TAG_PATIENT_ID ||
+                  tags[i] == DICOM_TAG_PATIENT_NAME ||
+                  tags[i] == DICOM_TAG_PATIENT_BIRTH_DATE));
+        }
+      }
+  
+      DicomMap::LoadMainDicomTags(tags, size, level);
+  
+      for (size_t i = 0; i < size; i++)
+      {
+        if (registry_.find(tags[i]) == registry_.end())
+        {
+          registry_[tags[i]] = TagInfo(level, DicomTagType_Main);
+        }
+      }
+    }
+
+  public:
+    MainDicomTagsRegistry()
+    {
+      LoadTags(ResourceType_Patient);
+      LoadTags(ResourceType_Study);
+      LoadTags(ResourceType_Series);
+      LoadTags(ResourceType_Instance); 
+    }
+
+    void LookupTag(ResourceType& level,
+                   DicomTagType& type,
+                   const DicomTag& tag) const
+    {
+      Registry::const_iterator it = registry_.find(tag);
+
+      if (it == registry_.end())
+      {
+        // Default values
+        level = ResourceType_Instance;
+        type = DicomTagType_Generic;
+      }
+      else
+      {
+        level = it->second.GetLevel();
+        type = it->second.GetType();
+      }
+    }
+  };
+
+
   bool ServerIndex::DeleteResource(Json::Value& target,
                                    const std::string& uuid,
                                    ResourceType expectedType)
@@ -541,7 +642,8 @@
     db_(db),
     maximumStorageSize_(0),
     maximumPatients_(0),
-    overwrite_(false)
+    overwrite_(false),
+    mainDicomTagsRegistry_(new MainDicomTagsRegistry)
   {
     listener_.reset(new Listener(context));
     db_.SetListener(*listener_);
@@ -2454,15 +2556,92 @@
   }
 
 
+  void ServerIndex::NormalizeLookup(DatabaseLookup& target,
+                                    const DatabaseLookup& source,
+                                    ResourceType queryLevel) const
+  {
+    assert(mainDicomTagsRegistry_.get() != NULL);
+    
+    for (size_t i = 0; i < source.GetConstraintsCount(); i++)
+    {
+      const DicomTagConstraint& constraint = source.GetConstraint(i);
+      
+      ResourceType tagLevel;
+      DicomTagType tagType;
+      mainDicomTagsRegistry_->LookupTag(tagLevel, tagType, constraint.GetTag());
+
+      if (IsResourceLevelAboveOrEqual(tagLevel, queryLevel) &&
+          (tagType == DicomTagType_Identifier ||
+           tagType == DicomTagType_Main))
+      {
+        if (tagType == DicomTagType_Identifier)
+        {
+          if (constraint.GetConstraintType() == ConstraintType_List)
+          {
+            if (!constraint.GetValues().empty())
+            {
+              std::auto_ptr<DicomTagConstraint> normalized(
+                new DicomTagConstraint(constraint.GetTag(),
+                                       ConstraintType_List, true,
+                                       constraint.IsMandatory()));
+
+              normalized->SetTagType(tagType);
+
+              for (std::set<std::string>::const_iterator
+                     value = constraint.GetValues().begin();
+                   value != constraint.GetValues().end(); ++value)
+              {
+                normalized->AddValue(ServerToolbox::NormalizeIdentifier(*value));
+              }
+
+              target.AddConstraint(normalized.release());
+            }
+          }
+          else
+          {
+            std::string value = ServerToolbox::NormalizeIdentifier(constraint.GetValue());
+            
+            std::auto_ptr<DicomTagConstraint> normalized(
+              new DicomTagConstraint(constraint.GetTag(),
+                                     constraint.GetConstraintType(),
+                                     value, true,
+                                     constraint.IsMandatory()));
+
+            normalized->SetTagType(tagType);
+            target.AddConstraint(normalized.release());
+          }
+        }
+        else if (tagType == DicomTagType_Main)
+        {
+          std::auto_ptr<DicomTagConstraint> clone(constraint.Clone());
+          clone->SetTagType(tagType);
+          target.AddConstraint(clone.release());
+        }
+        else if (tagType == DicomTagType_Generic)
+        {
+          // This tag is not indexed in the database, skip it
+        }
+        else
+        {
+          throw OrthancException(ErrorCode_InternalError);
+        }
+      }
+    }
+  }
+
+
   void ServerIndex::ApplyLookupPatients(std::vector<std::string>& patientsId,
                                         std::vector<std::string>& instancesId,
                                         const DatabaseLookup& lookup,
                                         size_t limit)
   {
-    boost::mutex::scoped_lock lock(mutex_);
-    
-    db_.ApplyLookupPatients(patientsId, lookup, limit);
-    db_.FindOneChildInstance(instancesId, patientsId, ResourceType_Patient);
+    DatabaseLookup normalized;
+    NormalizeLookup(normalized, lookup, ResourceType_Patient);
+
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+      db_.ApplyLookupPatients(patientsId, instancesId, normalized, limit);
+    }
   }
 
   
@@ -2472,9 +2651,12 @@
                                          ResourceType queryLevel,
                                          size_t limit)
   {
-    boost::mutex::scoped_lock lock(mutex_);
-
-    db_.ApplyLookupResources(resourcesId, lookup, queryLevel, limit);
-    db_.FindOneChildInstance(instancesId, resourcesId, queryLevel);
+    DatabaseLookup normalized;
+    NormalizeLookup(normalized, lookup, queryLevel);
+
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+      db_.ApplyLookupResources(resourcesId, instancesId, normalized, queryLevel, limit);
+    }
   }
 }
--- a/OrthancServer/ServerIndex.h	Mon Dec 17 17:05:28 2018 +0100
+++ b/OrthancServer/ServerIndex.h	Tue Dec 18 12:50:27 2018 +0100
@@ -59,6 +59,7 @@
     class Listener;
     class Transaction;
     class UnstableResourcePayload;
+    class MainDicomTagsRegistry;
 
     bool done_;
     boost::mutex mutex_;
@@ -72,6 +73,7 @@
     uint64_t     maximumStorageSize_;
     unsigned int maximumPatients_;
     bool         overwrite_;
+    std::auto_ptr<MainDicomTagsRegistry>  mainDicomTagsRegistry_;
 
     static void FlushThread(ServerIndex* that,
                             unsigned int threadSleep);
@@ -125,6 +127,10 @@
                              MetadataType metadata,
                              const std::string& value);
 
+    void NormalizeLookup(DatabaseLookup& target,
+                         const DatabaseLookup& source,
+                         ResourceType level) const;
+
   public:
     ServerIndex(ServerContext& context,
                 IDatabaseWrapper& database,
--- a/Plugins/Engine/OrthancPluginDatabase.cpp	Mon Dec 17 17:05:28 2018 +0100
+++ b/Plugins/Engine/OrthancPluginDatabase.cpp	Tue Dec 18 12:50:27 2018 +0100
@@ -1162,15 +1162,8 @@
   }
 
 
-  void OrthancPluginDatabase::FindOneChildInstance(std::vector<std::string>& instancesId,
-                                                   const std::vector<std::string>& resourcesId,
-                                                   ResourceType level)
-  {
-    throw OrthancException(ErrorCode_NotImplemented);
-  }
-
-
   void OrthancPluginDatabase::ApplyLookupPatients(std::vector<std::string>& patientsId,
+                                                  std::vector<std::string>& instancesId,
                                                   const DatabaseLookup& lookup,
                                                   size_t limit)
   {
@@ -1179,6 +1172,7 @@
   
 
   void OrthancPluginDatabase::ApplyLookupResources(std::vector<std::string>& patientsId,
+                                                   std::vector<std::string>& instancesId,
                                                    const DatabaseLookup& lookup,
                                                    ResourceType queryLevel,
                                                    size_t limit)
--- a/Plugins/Engine/OrthancPluginDatabase.h	Mon Dec 17 17:05:28 2018 +0100
+++ b/Plugins/Engine/OrthancPluginDatabase.h	Tue Dec 18 12:50:27 2018 +0100
@@ -271,15 +271,13 @@
 
     virtual bool IsDiskSizeAbove(uint64_t threshold);
 
-    virtual void FindOneChildInstance(std::vector<std::string>& instancesId,
-                                      const std::vector<std::string>& resourcesId,
-                                      ResourceType level);
-
     virtual void ApplyLookupPatients(std::vector<std::string>& patientsId,
+                                     std::vector<std::string>& instancesId,
                                      const DatabaseLookup& lookup,
                                      size_t limit);
 
     virtual void ApplyLookupResources(std::vector<std::string>& patientsId,
+                                      std::vector<std::string>& instancesId,
                                       const DatabaseLookup& lookup,
                                       ResourceType queryLevel,
                                       size_t limit);
--- a/UnitTestsSources/DatabaseLookupTests.cpp	Mon Dec 17 17:05:28 2018 +0100
+++ b/UnitTestsSources/DatabaseLookupTests.cpp	Tue Dec 18 12:50:27 2018 +0100
@@ -55,7 +55,7 @@
     ASSERT_FALSE(tag.IsMatch("hello"));
 
     ASSERT_TRUE(tag.IsCaseSensitive());
-    ASSERT_EQ(ConstraintType_Equal, tag.GetType());
+    ASSERT_EQ(ConstraintType_Equal, tag.GetConstraintType());
 
     DicomMap m;
     ASSERT_FALSE(tag.IsMatch(m));
@@ -91,7 +91,7 @@
     ASSERT_TRUE(tag.IsMatch("hello"));
 
     ASSERT_FALSE(tag.IsCaseSensitive());
-    ASSERT_EQ(ConstraintType_Wildcard, tag.GetType());
+    ASSERT_EQ(ConstraintType_Wildcard, tag.GetConstraintType());
   }
 
   {
@@ -150,7 +150,7 @@
     DatabaseLookup lookup;
     lookup.AddDicomConstraint(DICOM_TAG_PATIENT_ID, "HELLO", true, true);
     ASSERT_EQ(1u, lookup.GetConstraintsCount());
-    ASSERT_EQ(ConstraintType_Equal, lookup.GetConstraint(0).GetType());
+    ASSERT_EQ(ConstraintType_Equal, lookup.GetConstraint(0).GetConstraintType());
     ASSERT_EQ("HELLO", lookup.GetConstraint(0).GetValue());
     ASSERT_TRUE(lookup.GetConstraint(0).IsCaseSensitive());
   }
@@ -185,7 +185,7 @@
     lookup.AddDicomConstraint(DICOM_TAG_SERIES_DESCRIPTION, "2012-2016", false, true);
 
     // This is not a data VR
-    ASSERT_EQ(ConstraintType_Equal, lookup.GetConstraint(0).GetType());
+    ASSERT_EQ(ConstraintType_Equal, lookup.GetConstraint(0).GetConstraintType());
   }
 
   {
@@ -195,12 +195,12 @@
     // This is a data VR => range is effective
     ASSERT_EQ(2u, lookup.GetConstraintsCount());
 
-    ASSERT_TRUE(lookup.GetConstraint(0).GetType() != lookup.GetConstraint(1).GetType());
+    ASSERT_TRUE(lookup.GetConstraint(0).GetConstraintType() != lookup.GetConstraint(1).GetConstraintType());
 
     for (size_t i = 0; i < 2; i++)
     {
-      ASSERT_TRUE(lookup.GetConstraint(i).GetType() == ConstraintType_SmallerOrEqual ||
-                  lookup.GetConstraint(i).GetType() == ConstraintType_GreaterOrEqual);
+      ASSERT_TRUE(lookup.GetConstraint(i).GetConstraintType() == ConstraintType_SmallerOrEqual ||
+                  lookup.GetConstraint(i).GetConstraintType() == ConstraintType_GreaterOrEqual);
     }
   }
 
@@ -209,7 +209,7 @@
     lookup.AddDicomConstraint(DICOM_TAG_PATIENT_BIRTH_DATE, "2012-", false, true);
 
     ASSERT_EQ(1u, lookup.GetConstraintsCount());
-    ASSERT_EQ(ConstraintType_GreaterOrEqual, lookup.GetConstraint(0).GetType());
+    ASSERT_EQ(ConstraintType_GreaterOrEqual, lookup.GetConstraint(0).GetConstraintType());
     ASSERT_EQ("2012", lookup.GetConstraint(0).GetValue());
   }
 
@@ -219,7 +219,7 @@
 
     ASSERT_EQ(1u, lookup.GetConstraintsCount());
     ASSERT_EQ(DICOM_TAG_PATIENT_BIRTH_DATE,  lookup.GetConstraint(0).GetTag());
-    ASSERT_EQ(ConstraintType_SmallerOrEqual, lookup.GetConstraint(0).GetType());
+    ASSERT_EQ(ConstraintType_SmallerOrEqual, lookup.GetConstraint(0).GetConstraintType());
     ASSERT_EQ("2016", lookup.GetConstraint(0).GetValue());
   }
 
@@ -229,7 +229,7 @@
 
     ASSERT_EQ(1u, lookup.GetConstraintsCount());
     ASSERT_EQ(DICOM_TAG_MODALITY,  lookup.GetConstraint(0).GetTag());
-    ASSERT_EQ(ConstraintType_List, lookup.GetConstraint(0).GetType());
+    ASSERT_EQ(ConstraintType_List, lookup.GetConstraint(0).GetConstraintType());
 
     const std::set<std::string>& values = lookup.GetConstraint(0).GetValues();
     ASSERT_EQ(2u, values.size());
@@ -244,7 +244,7 @@
 
     ASSERT_EQ(1u, lookup.GetConstraintsCount());
     ASSERT_EQ(DICOM_TAG_STUDY_DESCRIPTION, lookup.GetConstraint(0).GetTag());
-    ASSERT_EQ(ConstraintType_List, lookup.GetConstraint(0).GetType());
+    ASSERT_EQ(ConstraintType_List, lookup.GetConstraint(0).GetConstraintType());
 
     const std::set<std::string>& values = lookup.GetConstraint(0).GetValues();
     ASSERT_EQ(2u, values.size());
@@ -258,7 +258,7 @@
     lookup.AddDicomConstraint(DICOM_TAG_STUDY_DESCRIPTION, "HE*O", false, true);
 
     ASSERT_EQ(1u, lookup.GetConstraintsCount());
-    ASSERT_EQ(ConstraintType_Wildcard, lookup.GetConstraint(0).GetType());
+    ASSERT_EQ(ConstraintType_Wildcard, lookup.GetConstraint(0).GetConstraintType());
   }
 
   {
@@ -266,7 +266,7 @@
     lookup.AddDicomConstraint(DICOM_TAG_STUDY_DESCRIPTION, "HE?O", false, true);
 
     ASSERT_EQ(1u, lookup.GetConstraintsCount());
-    ASSERT_EQ(ConstraintType_Wildcard, lookup.GetConstraint(0).GetType());
+    ASSERT_EQ(ConstraintType_Wildcard, lookup.GetConstraint(0).GetConstraintType());
   }
 
   {
--- a/UnitTestsSources/UnitTestsMain.cpp	Mon Dec 17 17:05:28 2018 +0100
+++ b/UnitTestsSources/UnitTestsMain.cpp	Tue Dec 18 12:50:27 2018 +0100
@@ -756,10 +756,29 @@
   ASSERT_EQ(MimeType_Xml, StringToMimeType("text/xml"));
   ASSERT_EQ(MimeType_Xml, StringToMimeType(EnumerationToString(MimeType_Xml)));
   ASSERT_THROW(StringToMimeType("nope"), OrthancException);
+
+  ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Patient, ResourceType_Patient));
+  ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Patient, ResourceType_Study));
+  ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Patient, ResourceType_Series));
+  ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Patient, ResourceType_Instance));
+
+  ASSERT_FALSE(IsResourceLevelAboveOrEqual(ResourceType_Study, ResourceType_Patient));
+  ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Study, ResourceType_Study));
+  ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Study, ResourceType_Series));
+  ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Study, ResourceType_Instance));
+
+  ASSERT_FALSE(IsResourceLevelAboveOrEqual(ResourceType_Series, ResourceType_Patient));
+  ASSERT_FALSE(IsResourceLevelAboveOrEqual(ResourceType_Series, ResourceType_Study));
+  ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Series, ResourceType_Series));
+  ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Series, ResourceType_Instance));
+
+  ASSERT_FALSE(IsResourceLevelAboveOrEqual(ResourceType_Instance, ResourceType_Patient));
+  ASSERT_FALSE(IsResourceLevelAboveOrEqual(ResourceType_Instance, ResourceType_Study));
+  ASSERT_FALSE(IsResourceLevelAboveOrEqual(ResourceType_Instance, ResourceType_Series));
+  ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Instance, ResourceType_Instance));
 }
 
 
-
 #if defined(__linux__) || defined(__OpenBSD__)
 #include <endian.h>
 #elif defined(__FreeBSD__)