changeset 6645:f0fec684ad91 case-sensitive-find

merged default -> case-sensitive-find
author Alain Mazy <am@orthanc.team>
date Tue, 31 Mar 2026 12:46:48 +0200
parents 742fb41bff59 (diff) 38f8a3b3d04e (current diff)
children
files NEWS
diffstat 9 files changed, 108 insertions(+), 34 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Mon Mar 30 14:21:48 2026 +0200
+++ b/NEWS	Tue Mar 31 12:46:48 2026 +0200
@@ -28,11 +28,15 @@
   - "orthanc_logged_errors_count" 
   - "orthanc_logged_warnings_count"
 * Fixed a security issue where one could hijack HTTP headers in the response
-  through the "filename" argument of "{...}/file" or "{...}/archive" routes.
-  (credits: Pavel Kohout, Aisle Research, https://www.aisle.com/)
-* In "tools/find", filtering against "LabelsConstraint": "None" with an empty "Labels" list
-  now returns all resources that do not have any label attached, instead of returning all resources.
-  This applies to the default SQLite DB and will apply to the next PostgreSQL plugin (v10.1).
+  through the `filename` argument of "/.../file" or "/.../archive" routes.
+  (credits: Pavel Kohout, Aisle Research. www.aisle.com)
+* In tools/find:
+  - Filtering against "LabelsConstraint": "None" with an empty "Labels" list
+    now returns all resources that do not have any labels attached instead of returning all resources.
+    This applies to the default SQLite DB and will apply to the next PostgreSQL plugin (v 10.1)
+  - Improved behavior wrt to non latin searches and `CaseSensitive` searches that can 
+    now be combined with `Since`.
+  WIP: this needs to be validated from a performance point of view !
 * New option "Utf8" available in the "{...}/archive" and "/tools/create-archive" routes
   to use UTF-8 filenames in the generated ZIP archives. It defaults to the value
   of the new configuration option "ZipUseUtf8".
--- a/OrthancFramework/Sources/Toolbox.cpp	Mon Mar 30 14:21:48 2026 +0200
+++ b/OrthancFramework/Sources/Toolbox.cpp	Tue Mar 31 12:46:48 2026 +0200
@@ -2065,8 +2065,7 @@
 #endif
   }
 
-
-  std::string Toolbox::ToUpperCaseWithAccents(const std::string& source)
+  static std::string ChangeCaseWithAccents(const std::string& source, bool toLowerCase)
   {
     bool error = (globalLocale_.get() == NULL);
 
@@ -2115,9 +2114,29 @@
      **/
 
     std::wstring w = boost::locale::conv::utf_to_utf<wchar_t>(source, boost::locale::conv::skip);
-    w = boost::algorithm::to_upper_copy<std::wstring>(w, *globalLocale_);
+    if (toLowerCase)
+    {
+      w = boost::algorithm::to_lower_copy<std::wstring>(w, *globalLocale_);
+    }
+    else
+    {
+      w = boost::algorithm::to_upper_copy<std::wstring>(w, *globalLocale_);
+    }
     return boost::locale::conv::utf_to_utf<char>(w, boost::locale::conv::skip);
   }
+
+
+  std::string Toolbox::ToUpperCaseWithAccents(const std::string& source)
+  {
+    return ChangeCaseWithAccents(source, false);
+  }
+
+
+  std::string Toolbox::ToLowerCaseWithAccents(const std::string& source)
+  {
+    return ChangeCaseWithAccents(source, true);
+  }
+
 #endif
 
 
--- a/OrthancFramework/Sources/Toolbox.h	Mon Mar 30 14:21:48 2026 +0200
+++ b/OrthancFramework/Sources/Toolbox.h	Tue Mar 31 12:46:48 2026 +0200
@@ -355,6 +355,8 @@
     static void FinalizeGlobalLocale();
 
     static std::string ToUpperCaseWithAccents(const std::string& source);
+
+    static std::string ToLowerCaseWithAccents(const std::string& source);
 #endif
 
     static void InitializeOpenSsl();
--- a/OrthancFramework/UnitTestsSources/FrameworkTests.cpp	Mon Mar 30 14:21:48 2026 +0200
+++ b/OrthancFramework/UnitTestsSources/FrameworkTests.cpp	Tue Mar 31 12:46:48 2026 +0200
@@ -489,6 +489,15 @@
   s = "CoU";
   Toolbox::ToLowerCase(s);
   ASSERT_EQ("cou", s);
+
+#if ORTHANC_ENABLE_LOCALE == 1
+  s = "éсйігдйомевінрие";
+  s = Toolbox::ToUpperCaseWithAccents(s);
+  ASSERT_EQ("ÉСЙІГДЙОМЕВІНРИЕ", s);
+
+  s = Toolbox::ToLowerCaseWithAccents(s);
+  ASSERT_EQ("éсйігдйомевінрие", s);
+#endif
 }
 
 
--- a/OrthancServer/Sources/Database/MainDicomTagsRegistry.cpp	Mon Mar 30 14:21:48 2026 +0200
+++ b/OrthancServer/Sources/Database/MainDicomTagsRegistry.cpp	Tue Mar 31 12:46:48 2026 +0200
@@ -129,19 +129,31 @@
         }
 
         bool isEquivalentConstraint;
-        
-        // DicomIdentifiers are stored UPPERCASE -> as soon as a case senstive search happens, it is currently not possible to perform it in DB only
+        std::unique_ptr<DatabaseDicomTagConstraint> dbConstraint;
+
+        // DicomIdentifiers are stored UPPERCASE -> as soon as a case senstive search happens, 
+        // it is currently not possible to perform it in DB only on the Identifiers table 
+        // but it can be performed on the MainDicomTags table -> transform the constraint
         if (type == DicomTagType_Identifier && source.GetConstraint(i).IsCaseSensitive())
         {
-          canBeFullyPerformedInDb = false;
+          dbConstraint.reset(source.GetConstraint(i).ConvertToDatabaseConstraint(isEquivalentConstraint, level, DicomTagType_Main));
+          canBeFullyPerformedInDb = true;
         }
-
-        target.AddConstraint(source.GetConstraint(i).ConvertToDatabaseConstraint(isEquivalentConstraint, level, type));
+        else
+        {
+          dbConstraint.reset(source.GetConstraint(i).ConvertToDatabaseConstraint(isEquivalentConstraint, level, type));
+          if (!isEquivalentConstraint && type == DicomTagType_Identifier)
+          {
+            dbConstraint.reset(source.GetConstraint(i).ConvertToDatabaseConstraint(isEquivalentConstraint, level, DicomTagType_Main));
+          }
+        }
 
         if (!isEquivalentConstraint)
         {
           isEquivalentLookup = false;
         }
+
+        target.AddConstraint(dbConstraint.release());
       }
       else
       {
--- a/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp	Mon Mar 30 14:21:48 2026 +0200
+++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp	Tue Mar 31 12:46:48 2026 +0200
@@ -226,6 +226,33 @@
   };
 
 
+  class SQLiteDatabaseWrapper::LowerWithAccents : public SQLite::IScalarFunction
+  {
+  public:
+    LowerWithAccents()
+    {
+    }
+
+    virtual const char* GetName() const ORTHANC_OVERRIDE
+    {
+      return "lower_with_accents";
+    }
+
+    virtual unsigned int GetCardinality() const ORTHANC_OVERRIDE
+    {
+      return 1;
+    }
+
+    virtual void Compute(SQLite::FunctionContext& context) ORTHANC_OVERRIDE
+    {
+      std::string source = context.GetStringValue(0);
+      std::string modified = Toolbox::ToLowerCaseWithAccents(source);
+
+      context.SetStringResult(modified);
+    }
+
+  };
+
   class SQLiteDatabaseWrapper::TransactionBase :
     public SQLiteDatabaseWrapper::UnitTestsTransaction,
     public Compatibility::ICreateInstance,
@@ -2769,7 +2796,7 @@
       signalRemainingAncestor_ = dynamic_cast<SignalRemainingAncestor*>(db_.Register(new SignalRemainingAncestor));
       db_.Register(new SignalFileDeleted(*this));
       db_.Register(new SignalResourceDeleted(*this));
-    
+      db_.Register(new LowerWithAccents());
       db_.Execute("PRAGMA ENCODING=\"UTF-8\";");
 
       // Performance tuning of SQLite with PRAGMAs
--- a/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.h	Mon Mar 30 14:21:48 2026 +0200
+++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.h	Tue Mar 31 12:46:48 2026 +0200
@@ -43,6 +43,7 @@
     class SignalFileDeleted;
     class SignalResourceDeleted;
     class SignalRemainingAncestor;
+    class LowerWithAccents;
     class ReadOnlyTransaction;
     class ReadWriteTransaction;
     class LookupFormatter;
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Mon Mar 30 14:21:48 2026 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Tue Mar 31 12:46:48 2026 +0200
@@ -3335,18 +3335,18 @@
         {
           caseSensitive = request[KEY_CASE_SENSITIVE].asBool();
 
-          if (requestType == FindType_Count && caseSensitive)
-          {
-            /**
-             * Explanation: "/tools/find" uses "lookup_->IsMatch(tags)" in "ResourceFinder::Execute()"
-             * to apply case sensitiveness (as the database stores tags with PN VR in lower case).
-             * But, the purpose of "/tools/count-resources" is to speed up the counting the number of
-             * matching resources: Calling "lookup_->IsMatch(tags)" would require gathering the main
-             * DICOM tags, which would lead to no speedup wrt. "/tools/find".
-             **/
-            throw OrthancException(ErrorCode_ParameterOutOfRange, "Setting \"" + std::string(KEY_CASE_SENSITIVE) +
-                                   "\" to \"true\" is not supported by /tools/count-resources");
-          }
+          // if (requestType == FindType_Count && caseSensitive)
+          // {
+          //   /**
+          //    * Explanation: "/tools/find" uses "lookup_->IsMatch(tags)" in "ResourceFinder::Execute()"
+          //    * to apply case sensitiveness (as the database stores tags with PN VR in lower case).
+          //    * But, the purpose of "/tools/count-resources" is to speed up the counting the number of
+          //    * matching resources: Calling "lookup_->IsMatch(tags)" would require gathering the main
+          //    * DICOM tags, which would lead to no speedup wrt. "/tools/find".
+          //    **/
+          //   throw OrthancException(ErrorCode_ParameterOutOfRange, "Setting \"" + std::string(KEY_CASE_SENSITIVE) +
+          //                          "\" to \"true\" is not supported by /tools/count-resources");
+          // }
         }
 
         { // DICOM Tag query
--- a/OrthancServer/Sources/Search/ISqlLookupFormatter.cpp	Mon Mar 30 14:21:48 2026 +0200
+++ b/OrthancServer/Sources/Search/ISqlLookupFormatter.cpp	Tue Mar 31 12:46:48 2026 +0200
@@ -123,7 +123,7 @@
         }
         else
         {
-          comparison = "lower(" + tag + ".value) " + op + " lower(" + parameter + ")";
+          comparison = "lower_with_accents(" + tag + ".value) " + op + " lower_with_accents(" + parameter + ")";
         }
 
         break;
@@ -146,7 +146,7 @@
           }
           else
           {
-            comparison += "lower(" + parameter + ")";
+            comparison += "lower_with_accents(" + parameter + ")";
           }
         }
 
@@ -156,7 +156,7 @@
         }
         else
         {
-          comparison = "lower(" +  tag + ".value) IN (" + comparison + ")";
+          comparison = "lower_with_accents(" +  tag + ".value) IN (" + comparison + ")";
         }
             
         break;
@@ -224,7 +224,7 @@
           }
           else
           {
-            comparison = ("lower(" + tag + ".value) LIKE lower(" +
+            comparison = ("lower_with_accents(" + tag + ".value) LIKE lower_with_accents(" +
                           parameter + ") " + formatter.FormatWildcardEscape());
           }
         }
@@ -451,7 +451,7 @@
         }
         else
         {
-          comparison = " AND lower(value) " + op + " lower(" + parameter + ")";
+          comparison = " AND lower_with_accents(value) " + op + " lower_with_accents(" + parameter + ")";
         }
 
         break;
@@ -470,7 +470,7 @@
           }
           else
           {
-            comparisonValues.push_back("lower(" + parameter + ")");
+            comparisonValues.push_back("lower_with_accents(" + parameter + ")");
           }
         }
 
@@ -483,7 +483,7 @@
         }
         else
         {
-          comparison = " AND lower(value) IN (" + values + ")";
+          comparison = " AND lower_with_accents(value) IN (" + values + ")";
         }
             
         break;
@@ -550,7 +550,7 @@
           }
           else
           {
-            comparison = " AND lower(value) LIKE lower(" + parameter + ") " + formatter.FormatWildcardEscape();
+            comparison = " AND lower_with_accents(value) LIKE lower_with_accents(" + parameter + ") " + formatter.FormatWildcardEscape();
           }
         }