Mercurial > hg > orthanc-databases
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 } |