comparison Framework/Plugins/ISqlLookupFormatter.cpp @ 547:b8e6e7a19424

un-sharing DatabaseConstraint and ISqlLookupFormatter with Orthanc core
author Sebastien Jodogne <s.jodogne@gmail.com>
date Mon, 09 Sep 2024 13:18:35 +0200
parents
children 25005693297b
comparison
equal deleted inserted replaced
542:a8f9d44e7842 547:b8e6e7a19424
1 /**
2 * Orthanc - A Lightweight, RESTful DICOM Store
3 * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
4 * Department, University Hospital of Liege, Belgium
5 * Copyright (C) 2017-2023 Osimis S.A., Belgium
6 * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
7 * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
8 *
9 * This program is free software: you can redistribute it and/or
10 * modify it under the terms of the GNU Affero General Public License
11 * as published by the Free Software Foundation, either version 3 of
12 * the License, or (at your option) any later version.
13 *
14 * This program is distributed in the hope that it will be useful, but
15 * WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17 * Affero General Public License for more details.
18 *
19 * You should have received a copy of the GNU Affero General Public License
20 * along with this program. If not, see <http://www.gnu.org/licenses/>.
21 **/
22
23
24 /**
25 * NB: Until 2024-09-09, this file was synchronized with the following
26 * folder from the Orthanc main project:
27 * https://orthanc.uclouvain.be/hg/orthanc/file/default/OrthancServer/Sources/Search/
28 **/
29
30
31 #if !defined(ORTHANC_BUILDING_SERVER_LIBRARY)
32 # error Macro ORTHANC_BUILDING_SERVER_LIBRARY must be defined
33 #endif
34
35 #if ORTHANC_BUILDING_SERVER_LIBRARY == 1
36 # include "../PrecompiledHeadersServer.h"
37 #endif
38
39 #include "ISqlLookupFormatter.h"
40
41 #if ORTHANC_BUILDING_SERVER_LIBRARY == 1
42 # include "../../../OrthancFramework/Sources/OrthancException.h"
43 # include "../../../OrthancFramework/Sources/Toolbox.h"
44 #else
45 # include <OrthancException.h>
46 # include <Toolbox.h>
47 #endif
48
49 #include "DatabaseConstraint.h"
50
51 #include <cassert>
52 #include <boost/lexical_cast.hpp>
53 #include <list>
54
55
56 namespace Orthanc
57 {
58 static std::string FormatLevel(ResourceType level)
59 {
60 switch (level)
61 {
62 case ResourceType_Patient:
63 return "patients";
64
65 case ResourceType_Study:
66 return "studies";
67
68 case ResourceType_Series:
69 return "series";
70
71 case ResourceType_Instance:
72 return "instances";
73
74 default:
75 throw OrthancException(ErrorCode_InternalError);
76 }
77 }
78
79
80 static bool FormatComparison(std::string& target,
81 ISqlLookupFormatter& formatter,
82 const DatabaseConstraint& constraint,
83 size_t index,
84 bool escapeBrackets)
85 {
86 std::string tag = "t" + boost::lexical_cast<std::string>(index);
87
88 std::string comparison;
89
90 switch (constraint.GetConstraintType())
91 {
92 case ConstraintType_Equal:
93 case ConstraintType_SmallerOrEqual:
94 case ConstraintType_GreaterOrEqual:
95 {
96 std::string op;
97 switch (constraint.GetConstraintType())
98 {
99 case ConstraintType_Equal:
100 op = "=";
101 break;
102
103 case ConstraintType_SmallerOrEqual:
104 op = "<=";
105 break;
106
107 case ConstraintType_GreaterOrEqual:
108 op = ">=";
109 break;
110
111 default:
112 throw OrthancException(ErrorCode_InternalError);
113 }
114
115 std::string parameter = formatter.GenerateParameter(constraint.GetSingleValue());
116
117 if (constraint.IsCaseSensitive())
118 {
119 comparison = tag + ".value " + op + " " + parameter;
120 }
121 else
122 {
123 comparison = "lower(" + tag + ".value) " + op + " lower(" + parameter + ")";
124 }
125
126 break;
127 }
128
129 case ConstraintType_List:
130 {
131 for (size_t i = 0; i < constraint.GetValuesCount(); i++)
132 {
133 if (!comparison.empty())
134 {
135 comparison += ", ";
136 }
137
138 std::string parameter = formatter.GenerateParameter(constraint.GetValue(i));
139
140 if (constraint.IsCaseSensitive())
141 {
142 comparison += parameter;
143 }
144 else
145 {
146 comparison += "lower(" + parameter + ")";
147 }
148 }
149
150 if (constraint.IsCaseSensitive())
151 {
152 comparison = tag + ".value IN (" + comparison + ")";
153 }
154 else
155 {
156 comparison = "lower(" + tag + ".value) IN (" + comparison + ")";
157 }
158
159 break;
160 }
161
162 case ConstraintType_Wildcard:
163 {
164 const std::string value = constraint.GetSingleValue();
165
166 if (value == "*")
167 {
168 if (!constraint.IsMandatory())
169 {
170 // Universal constraint on an optional tag, ignore it
171 return false;
172 }
173 }
174 else
175 {
176 std::string escaped;
177 escaped.reserve(value.size());
178
179 for (size_t i = 0; i < value.size(); i++)
180 {
181 if (value[i] == '*')
182 {
183 escaped += "%";
184 }
185 else if (value[i] == '?')
186 {
187 escaped += "_";
188 }
189 else if (value[i] == '%')
190 {
191 escaped += "\\%";
192 }
193 else if (value[i] == '_')
194 {
195 escaped += "\\_";
196 }
197 else if (value[i] == '\\')
198 {
199 escaped += "\\\\";
200 }
201 else if (escapeBrackets && value[i] == '[')
202 {
203 escaped += "\\[";
204 }
205 else if (escapeBrackets && value[i] == ']')
206 {
207 escaped += "\\]";
208 }
209 else
210 {
211 escaped += value[i];
212 }
213 }
214
215 std::string parameter = formatter.GenerateParameter(escaped);
216
217 if (constraint.IsCaseSensitive())
218 {
219 comparison = (tag + ".value LIKE " + parameter + " " +
220 formatter.FormatWildcardEscape());
221 }
222 else
223 {
224 comparison = ("lower(" + tag + ".value) LIKE lower(" +
225 parameter + ") " + formatter.FormatWildcardEscape());
226 }
227 }
228
229 break;
230 }
231
232 default:
233 return false;
234 }
235
236 if (constraint.IsMandatory())
237 {
238 target = comparison;
239 }
240 else if (comparison.empty())
241 {
242 target = tag + ".value IS NULL";
243 }
244 else
245 {
246 target = tag + ".value IS NULL OR " + comparison;
247 }
248
249 return true;
250 }
251
252
253 static void FormatJoin(std::string& target,
254 const DatabaseConstraint& constraint,
255 size_t index)
256 {
257 std::string tag = "t" + boost::lexical_cast<std::string>(index);
258
259 if (constraint.IsMandatory())
260 {
261 target = " INNER JOIN ";
262 }
263 else
264 {
265 target = " LEFT JOIN ";
266 }
267
268 if (constraint.IsIdentifier())
269 {
270 target += "DicomIdentifiers ";
271 }
272 else
273 {
274 target += "MainDicomTags ";
275 }
276
277 target += (tag + " ON " + tag + ".id = " + FormatLevel(constraint.GetLevel()) +
278 ".internalId AND " + tag + ".tagGroup = " +
279 boost::lexical_cast<std::string>(constraint.GetTag().GetGroup()) +
280 " AND " + tag + ".tagElement = " +
281 boost::lexical_cast<std::string>(constraint.GetTag().GetElement()));
282 }
283
284
285 static std::string Join(const std::list<std::string>& values,
286 const std::string& prefix,
287 const std::string& separator)
288 {
289 if (values.empty())
290 {
291 return "";
292 }
293 else
294 {
295 std::string s = prefix;
296
297 bool first = true;
298 for (std::list<std::string>::const_iterator it = values.begin(); it != values.end(); ++it)
299 {
300 if (first)
301 {
302 first = false;
303 }
304 else
305 {
306 s += separator;
307 }
308
309 s += *it;
310 }
311
312 return s;
313 }
314 }
315
316 static bool FormatComparison2(std::string& target,
317 ISqlLookupFormatter& formatter,
318 const DatabaseConstraint& constraint,
319 bool escapeBrackets)
320 {
321 std::string comparison;
322 std::string tagFilter = ("tagGroup = " + boost::lexical_cast<std::string>(constraint.GetTag().GetGroup())
323 + " AND tagElement = " + boost::lexical_cast<std::string>(constraint.GetTag().GetElement()));
324
325 switch (constraint.GetConstraintType())
326 {
327 case ConstraintType_Equal:
328 case ConstraintType_SmallerOrEqual:
329 case ConstraintType_GreaterOrEqual:
330 {
331 std::string op;
332 switch (constraint.GetConstraintType())
333 {
334 case ConstraintType_Equal:
335 op = "=";
336 break;
337
338 case ConstraintType_SmallerOrEqual:
339 op = "<=";
340 break;
341
342 case ConstraintType_GreaterOrEqual:
343 op = ">=";
344 break;
345
346 default:
347 throw OrthancException(ErrorCode_InternalError);
348 }
349
350 std::string parameter = formatter.GenerateParameter(constraint.GetSingleValue());
351
352 if (constraint.IsCaseSensitive())
353 {
354 comparison = " AND value " + op + " " + parameter;
355 }
356 else
357 {
358 comparison = " AND lower(value) " + op + " lower(" + parameter + ")";
359 }
360
361 break;
362 }
363
364 case ConstraintType_List:
365 {
366 std::vector<std::string> comparisonValues;
367 for (size_t i = 0; i < constraint.GetValuesCount(); i++)
368 {
369 std::string parameter = formatter.GenerateParameter(constraint.GetValue(i));
370
371 if (constraint.IsCaseSensitive())
372 {
373 comparisonValues.push_back(parameter);
374 }
375 else
376 {
377 comparisonValues.push_back("lower(" + parameter + ")");
378 }
379 }
380
381 std::string values;
382 Toolbox::JoinStrings(values, comparisonValues, ", ");
383
384 if (constraint.IsCaseSensitive())
385 {
386 comparison = " AND value IN (" + values + ")";
387 }
388 else
389 {
390 comparison = " AND lower(value) IN (" + values + ")";
391 }
392
393 break;
394 }
395
396 case ConstraintType_Wildcard:
397 {
398 const std::string value = constraint.GetSingleValue();
399
400 if (value == "*")
401 {
402 if (!constraint.IsMandatory())
403 {
404 // Universal constraint on an optional tag, ignore it
405 return false;
406 }
407 }
408 else
409 {
410 std::string escaped;
411 escaped.reserve(value.size());
412
413 for (size_t i = 0; i < value.size(); i++)
414 {
415 if (value[i] == '*')
416 {
417 escaped += "%";
418 }
419 else if (value[i] == '?')
420 {
421 escaped += "_";
422 }
423 else if (value[i] == '%')
424 {
425 escaped += "\\%";
426 }
427 else if (value[i] == '_')
428 {
429 escaped += "\\_";
430 }
431 else if (value[i] == '\\')
432 {
433 escaped += "\\\\";
434 }
435 else if (escapeBrackets && value[i] == '[')
436 {
437 escaped += "\\[";
438 }
439 else if (escapeBrackets && value[i] == ']')
440 {
441 escaped += "\\]";
442 }
443 else
444 {
445 escaped += value[i];
446 }
447 }
448
449 std::string parameter = formatter.GenerateParameter(escaped);
450
451 if (constraint.IsCaseSensitive())
452 {
453 comparison = " AND value LIKE " + parameter + " " + formatter.FormatWildcardEscape();
454 }
455 else
456 {
457 comparison = " AND lower(value) LIKE lower(" + parameter + ") " + formatter.FormatWildcardEscape();
458 }
459 }
460
461 break;
462 }
463
464 default:
465 return false;
466 }
467
468 if (constraint.IsMandatory())
469 {
470 target = tagFilter + comparison;
471 }
472 else if (comparison.empty())
473 {
474 target = tagFilter + " AND value IS NULL";
475 }
476 else
477 {
478 target = tagFilter + " AND value IS NULL OR " + comparison;
479 }
480
481 return true;
482 }
483
484
485 void ISqlLookupFormatter::GetLookupLevels(ResourceType& lowerLevel,
486 ResourceType& upperLevel,
487 const ResourceType& queryLevel,
488 const DatabaseConstraints& lookup)
489 {
490 assert(ResourceType_Patient < ResourceType_Study &&
491 ResourceType_Study < ResourceType_Series &&
492 ResourceType_Series < ResourceType_Instance);
493
494 lowerLevel = queryLevel;
495 upperLevel = queryLevel;
496
497 for (size_t i = 0; i < lookup.GetSize(); i++)
498 {
499 ResourceType level = lookup.GetConstraint(i).GetLevel();
500
501 if (level < upperLevel)
502 {
503 upperLevel = level;
504 }
505
506 if (level > lowerLevel)
507 {
508 lowerLevel = level;
509 }
510 }
511 }
512
513
514 void ISqlLookupFormatter::Apply(std::string& sql,
515 ISqlLookupFormatter& formatter,
516 const DatabaseConstraints& lookup,
517 ResourceType queryLevel,
518 const std::set<std::string>& labels,
519 LabelsConstraint labelsConstraint,
520 size_t limit)
521 {
522 ResourceType lowerLevel, upperLevel;
523 GetLookupLevels(lowerLevel, upperLevel, queryLevel, lookup);
524
525 assert(upperLevel <= queryLevel &&
526 queryLevel <= lowerLevel);
527
528 const bool escapeBrackets = formatter.IsEscapeBrackets();
529
530 std::string joins, comparisons;
531
532 size_t count = 0;
533
534 for (size_t i = 0; i < lookup.GetSize(); i++)
535 {
536 const DatabaseConstraint& constraint = lookup.GetConstraint(i);
537
538 std::string comparison;
539
540 if (FormatComparison(comparison, formatter, constraint, count, escapeBrackets))
541 {
542 std::string join;
543 FormatJoin(join, constraint, count);
544 joins += join;
545
546 if (!comparison.empty())
547 {
548 comparisons += " AND " + comparison;
549 }
550
551 count ++;
552 }
553 }
554
555 sql = ("SELECT " +
556 FormatLevel(queryLevel) + ".publicId, " +
557 FormatLevel(queryLevel) + ".internalId" +
558 " FROM Resources AS " + FormatLevel(queryLevel));
559
560 for (int level = queryLevel - 1; level >= upperLevel; level--)
561 {
562 sql += (" INNER JOIN Resources " +
563 FormatLevel(static_cast<ResourceType>(level)) + " ON " +
564 FormatLevel(static_cast<ResourceType>(level)) + ".internalId=" +
565 FormatLevel(static_cast<ResourceType>(level + 1)) + ".parentId");
566 }
567
568 for (int level = queryLevel + 1; level <= lowerLevel; level++)
569 {
570 sql += (" INNER JOIN Resources " +
571 FormatLevel(static_cast<ResourceType>(level)) + " ON " +
572 FormatLevel(static_cast<ResourceType>(level - 1)) + ".internalId=" +
573 FormatLevel(static_cast<ResourceType>(level)) + ".parentId");
574 }
575
576 std::list<std::string> where;
577 where.push_back(FormatLevel(queryLevel) + ".resourceType = " +
578 formatter.FormatResourceType(queryLevel) + comparisons);
579
580 if (!labels.empty())
581 {
582 /**
583 * "In SQL Server, NOT EXISTS and NOT IN predicates are the best
584 * way to search for missing values, as long as both columns in
585 * question are NOT NULL."
586 * https://explainextended.com/2009/09/15/not-in-vs-not-exists-vs-left-join-is-null-sql-server/
587 **/
588
589 std::list<std::string> formattedLabels;
590 for (std::set<std::string>::const_iterator it = labels.begin(); it != labels.end(); ++it)
591 {
592 formattedLabels.push_back(formatter.GenerateParameter(*it));
593 }
594
595 std::string condition;
596 switch (labelsConstraint)
597 {
598 case LabelsConstraint_Any:
599 condition = "> 0";
600 break;
601
602 case LabelsConstraint_All:
603 condition = "= " + boost::lexical_cast<std::string>(labels.size());
604 break;
605
606 case LabelsConstraint_None:
607 condition = "= 0";
608 break;
609
610 default:
611 throw OrthancException(ErrorCode_ParameterOutOfRange);
612 }
613
614 where.push_back("(SELECT COUNT(1) FROM Labels AS selectedLabels WHERE selectedLabels.id = " + FormatLevel(queryLevel) +
615 ".internalId AND selectedLabels.label IN (" + Join(formattedLabels, "", ", ") + ")) " + condition);
616 }
617
618 sql += joins + Join(where, " WHERE ", " AND ");
619
620 if (limit != 0)
621 {
622 sql += " LIMIT " + boost::lexical_cast<std::string>(limit);
623 }
624 }
625
626
627 void ISqlLookupFormatter::ApplySingleLevel(std::string& sql,
628 ISqlLookupFormatter& formatter,
629 const DatabaseConstraints& lookup,
630 ResourceType queryLevel,
631 const std::set<std::string>& labels,
632 LabelsConstraint labelsConstraint,
633 size_t limit
634 )
635 {
636 ResourceType lowerLevel, upperLevel;
637 GetLookupLevels(lowerLevel, upperLevel, queryLevel, lookup);
638
639 assert(upperLevel == queryLevel &&
640 queryLevel == lowerLevel);
641
642 const bool escapeBrackets = formatter.IsEscapeBrackets();
643
644 std::vector<std::string> mainDicomTagsComparisons, dicomIdentifiersComparisons;
645
646 for (size_t i = 0; i < lookup.GetSize(); i++)
647 {
648 const DatabaseConstraint& constraint = lookup.GetConstraint(i);
649
650 std::string comparison;
651
652 if (FormatComparison2(comparison, formatter, constraint, escapeBrackets))
653 {
654 if (!comparison.empty())
655 {
656 if (constraint.IsIdentifier())
657 {
658 dicomIdentifiersComparisons.push_back(comparison);
659 }
660 else
661 {
662 mainDicomTagsComparisons.push_back(comparison);
663 }
664 }
665 }
666 }
667
668 sql = ("SELECT publicId, internalId "
669 "FROM Resources "
670 "WHERE resourceType = " + formatter.FormatResourceType(queryLevel)
671 + " ");
672
673 if (dicomIdentifiersComparisons.size() > 0)
674 {
675 for (std::vector<std::string>::const_iterator it = dicomIdentifiersComparisons.begin(); it < dicomIdentifiersComparisons.end(); ++it)
676 {
677 sql += (" AND internalId IN (SELECT id FROM DicomIdentifiers WHERE " + *it + ") ");
678 }
679 }
680
681 if (mainDicomTagsComparisons.size() > 0)
682 {
683 for (std::vector<std::string>::const_iterator it = mainDicomTagsComparisons.begin(); it < mainDicomTagsComparisons.end(); ++it)
684 {
685 sql += (" AND internalId IN (SELECT id FROM MainDicomTags WHERE " + *it + ") ");
686 }
687 }
688
689 if (!labels.empty())
690 {
691 /**
692 * "In SQL Server, NOT EXISTS and NOT IN predicates are the best
693 * way to search for missing values, as long as both columns in
694 * question are NOT NULL."
695 * https://explainextended.com/2009/09/15/not-in-vs-not-exists-vs-left-join-is-null-sql-server/
696 **/
697
698 std::list<std::string> formattedLabels;
699 for (std::set<std::string>::const_iterator it = labels.begin(); it != labels.end(); ++it)
700 {
701 formattedLabels.push_back(formatter.GenerateParameter(*it));
702 }
703
704 std::string condition;
705 std::string inOrNotIn;
706 switch (labelsConstraint)
707 {
708 case LabelsConstraint_Any:
709 condition = "> 0";
710 inOrNotIn = "IN";
711 break;
712
713 case LabelsConstraint_All:
714 condition = "= " + boost::lexical_cast<std::string>(labels.size());
715 inOrNotIn = "IN";
716 break;
717
718 case LabelsConstraint_None:
719 condition = "> 0";
720 inOrNotIn = "NOT IN";
721 break;
722
723 default:
724 throw OrthancException(ErrorCode_ParameterOutOfRange);
725 }
726
727 sql += (" AND internalId " + inOrNotIn + " (SELECT id"
728 " FROM (SELECT id, COUNT(1) AS labelsCount "
729 "FROM Labels "
730 "WHERE label IN (" + Join(formattedLabels, "", ", ") + ") GROUP BY id"
731 ") AS temp "
732 " WHERE labelsCount " + condition + ")");
733 }
734
735 if (limit != 0)
736 {
737 sql += " LIMIT " + boost::lexical_cast<std::string>(limit);
738 }
739 }
740
741 }