Orthanc/OrthancServer/Sources/Search/ISqlLookupFormatter.cpp
2025-06-23 19:07:37 +05:30

1085 lines
33 KiB
C++

/**
* Orthanc - A Lightweight, RESTful DICOM Store
* Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
* Department, University Hospital of Liege, Belgium
* Copyright (C) 2017-2023 Osimis S.A., Belgium
* Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
* Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
*
* This program is free software: you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
**/
#include "../PrecompiledHeadersServer.h"
#include "ISqlLookupFormatter.h"
#include "../../../OrthancFramework/Sources/OrthancException.h"
#include "../../../OrthancFramework/Sources/Toolbox.h"
#include "../Database/FindRequest.h"
#include "DatabaseDicomTagConstraint.h"
#include "../Database/MainDicomTagsRegistry.h"
#include <cassert>
#include <boost/lexical_cast.hpp>
#include <list>
namespace Orthanc
{
static std::string FormatLevel(ResourceType level)
{
switch (level)
{
case ResourceType_Patient:
return "patients";
case ResourceType_Study:
return "studies";
case ResourceType_Series:
return "series";
case ResourceType_Instance:
return "instances";
default:
throw OrthancException(ErrorCode_InternalError);
}
}
static std::string FormatLevel(const char* prefix, ResourceType level)
{
switch (level)
{
case ResourceType_Patient:
return std::string(prefix) + "patients";
case ResourceType_Study:
return std::string(prefix) + "studies";
case ResourceType_Series:
return std::string(prefix) + "series";
case ResourceType_Instance:
return std::string(prefix) + "instances";
default:
throw OrthancException(ErrorCode_InternalError);
}
}
static bool FormatComparison(std::string& target,
ISqlLookupFormatter& formatter,
const IDatabaseConstraint& constraint,
size_t index,
bool escapeBrackets)
{
std::string tag = "t" + boost::lexical_cast<std::string>(index);
std::string comparison;
switch (constraint.GetConstraintType())
{
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);
}
std::string parameter = formatter.GenerateParameter(constraint.GetSingleValue());
if (constraint.IsCaseSensitive())
{
comparison = tag + ".value " + op + " " + parameter;
}
else
{
comparison = "lower(" + tag + ".value) " + op + " lower(" + parameter + ")";
}
break;
}
case ConstraintType_List:
{
for (size_t i = 0; i < constraint.GetValuesCount(); i++)
{
if (!comparison.empty())
{
comparison += ", ";
}
std::string parameter = formatter.GenerateParameter(constraint.GetValue(i));
if (constraint.IsCaseSensitive())
{
comparison += parameter;
}
else
{
comparison += "lower(" + parameter + ")";
}
}
if (constraint.IsCaseSensitive())
{
comparison = tag + ".value IN (" + comparison + ")";
}
else
{
comparison = "lower(" + tag + ".value) IN (" + comparison + ")";
}
break;
}
case ConstraintType_Wildcard:
{
const std::string value = constraint.GetSingleValue();
if (value == "*")
{
if (!constraint.IsMandatory())
{
// Universal constraint on an optional tag, ignore it
return false;
}
}
else
{
std::string escaped;
escaped.reserve(value.size());
for (size_t i = 0; i < value.size(); i++)
{
if (value[i] == '*')
{
escaped += "%";
}
else if (value[i] == '?')
{
escaped += "_";
}
else if (value[i] == '%')
{
escaped += "\\%";
}
else if (value[i] == '_')
{
escaped += "\\_";
}
else if (value[i] == '\\')
{
escaped += "\\\\";
}
else if (escapeBrackets && value[i] == '[')
{
escaped += "\\[";
}
else if (escapeBrackets && value[i] == ']')
{
escaped += "\\]";
}
else
{
escaped += value[i];
}
}
std::string parameter = formatter.GenerateParameter(escaped);
if (constraint.IsCaseSensitive())
{
comparison = (tag + ".value LIKE " + parameter + " " +
formatter.FormatWildcardEscape());
}
else
{
comparison = ("lower(" + tag + ".value) LIKE lower(" +
parameter + ") " + formatter.FormatWildcardEscape());
}
}
break;
}
default:
return false;
}
if (constraint.IsMandatory())
{
target = comparison;
}
else if (comparison.empty())
{
target = tag + ".value IS NULL";
}
else
{
target = tag + ".value IS NULL OR " + comparison;
}
return true;
}
static void FormatJoin(std::string& target,
const DatabaseDicomTagConstraint& constraint,
size_t index)
{
std::string tag = "t" + boost::lexical_cast<std::string>(index);
if (constraint.IsMandatory())
{
target = " INNER JOIN ";
}
else
{
target = " LEFT JOIN ";
}
if (constraint.IsIdentifier())
{
target += "DicomIdentifiers ";
}
else
{
target += "MainDicomTags ";
}
target += (tag + " ON " + tag + ".id = " + FormatLevel(constraint.GetLevel()) +
".internalId AND " + tag + ".tagGroup = " +
boost::lexical_cast<std::string>(constraint.GetTag().GetGroup()) +
" AND " + tag + ".tagElement = " +
boost::lexical_cast<std::string>(constraint.GetTag().GetElement()));
}
static void FormatJoin(std::string& target,
const DatabaseMetadataConstraint& constraint,
ResourceType level,
size_t index)
{
std::string tag = "t" + boost::lexical_cast<std::string>(index);
if (constraint.IsMandatory())
{
target = " INNER JOIN ";
}
else
{
target = " LEFT JOIN ";
}
target += "Metadata ";
target += tag + " ON " + tag + ".id = " + FormatLevel(level) +
".internalId AND " + tag + ".type = " +
boost::lexical_cast<std::string>(constraint.GetMetadata());
}
static void FormatJoinForOrdering(std::string& target,
const DicomTag& tag,
size_t index,
ResourceType requestLevel)
{
std::string orderArg = "order" + boost::lexical_cast<std::string>(index);
target.clear();
ResourceType tagLevel;
DicomTagType tagType;
MainDicomTagsRegistry registry;
registry.LookupTag(tagLevel, tagType, tag);
if (tagLevel == ResourceType_Patient && requestLevel == ResourceType_Study)
{ // Patient tags are copied at study level
tagLevel = ResourceType_Study;
}
std::string tagTable;
if (tagType == DicomTagType_Identifier)
{
tagTable = "DicomIdentifiers ";
}
else
{
tagTable = "MainDicomTags ";
}
std::string tagFilter = orderArg + ".tagGroup = " + boost::lexical_cast<std::string>(tag.GetGroup()) + " AND " + orderArg + ".tagElement = " + boost::lexical_cast<std::string>(tag.GetElement());
if (tagLevel == requestLevel)
{
target = " LEFT JOIN " + tagTable + " " + orderArg + " ON " + orderArg + ".id = " + FormatLevel(requestLevel) +
".internalId AND " + tagFilter;
}
else if (static_cast<int32_t>(requestLevel) - static_cast<int32_t>(tagLevel) == 1)
{
target = " INNER JOIN Resources " + orderArg + "parent ON " + orderArg + "parent.internalId = " + FormatLevel(requestLevel) + ".parentId "
" LEFT JOIN " + tagTable + " " + orderArg + " ON " + orderArg + ".id = " + orderArg + "parent.internalId AND " + tagFilter;
}
else if (static_cast<int32_t>(requestLevel) - static_cast<int32_t>(tagLevel) == 2)
{
target = " INNER JOIN Resources " + orderArg + "parent ON " + orderArg + "parent.internalId = " + FormatLevel(requestLevel) + ".parentId "
" INNER JOIN Resources " + orderArg + "grandparent ON " + orderArg + "grandparent.internalId = " + orderArg + "parent.parentId "
" LEFT JOIN " + tagTable + " " + orderArg + " ON " + orderArg + ".id = " + orderArg + "grandparent.internalId AND " + tagFilter;
}
else if (static_cast<int32_t>(requestLevel) - static_cast<int32_t>(tagLevel) == 3)
{
target = " INNER JOIN Resources " + orderArg + "parent ON " + orderArg + "parent.internalId = " + FormatLevel(requestLevel) + ".parentId "
" INNER JOIN Resources " + orderArg + "grandparent ON " + orderArg + "grandparent.internalId = " + orderArg + "parent.parentId "
" INNER JOIN Resources " + orderArg + "grandgrandparent ON " + orderArg + "grandgrandparent.internalId = " + orderArg + "grandparent.parentId "
" LEFT JOIN " + tagTable + " " + orderArg + " ON " + orderArg + ".id = " + orderArg + "grandgrandparent.internalId AND " + tagFilter;
}
}
static void FormatJoinForOrdering(std::string& target,
const MetadataType& metadata,
size_t index,
ResourceType requestLevel)
{
std::string arg = "order" + boost::lexical_cast<std::string>(index);
target = " LEFT JOIN Metadata " + arg + " ON " + arg + ".id = " + FormatLevel(requestLevel) +
".internalId AND " + arg + ".type = " +
boost::lexical_cast<std::string>(metadata);
}
static std::string Join(const std::list<std::string>& values,
const std::string& prefix,
const std::string& separator)
{
if (values.empty())
{
return "";
}
else
{
std::string s = prefix;
bool first = true;
for (std::list<std::string>::const_iterator it = values.begin(); it != values.end(); ++it)
{
if (first)
{
first = false;
}
else
{
s += separator;
}
s += *it;
}
return s;
}
}
static bool FormatComparison2(std::string& target,
ISqlLookupFormatter& formatter,
const DatabaseDicomTagConstraint& constraint,
bool escapeBrackets)
{
std::string comparison;
std::string tagFilter = ("tagGroup = " + boost::lexical_cast<std::string>(constraint.GetTag().GetGroup())
+ " AND tagElement = " + boost::lexical_cast<std::string>(constraint.GetTag().GetElement()));
switch (constraint.GetConstraintType())
{
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);
}
std::string parameter = formatter.GenerateParameter(constraint.GetSingleValue());
if (constraint.IsCaseSensitive())
{
comparison = " AND value " + op + " " + parameter;
}
else
{
comparison = " AND lower(value) " + op + " lower(" + parameter + ")";
}
break;
}
case ConstraintType_List:
{
std::vector<std::string> comparisonValues;
for (size_t i = 0; i < constraint.GetValuesCount(); i++)
{
std::string parameter = formatter.GenerateParameter(constraint.GetValue(i));
if (constraint.IsCaseSensitive())
{
comparisonValues.push_back(parameter);
}
else
{
comparisonValues.push_back("lower(" + parameter + ")");
}
}
std::string values;
Toolbox::JoinStrings(values, comparisonValues, ", ");
if (constraint.IsCaseSensitive())
{
comparison = " AND value IN (" + values + ")";
}
else
{
comparison = " AND lower(value) IN (" + values + ")";
}
break;
}
case ConstraintType_Wildcard:
{
const std::string value = constraint.GetSingleValue();
if (value == "*")
{
if (!constraint.IsMandatory())
{
// Universal constraint on an optional tag, ignore it
return false;
}
}
else
{
std::string escaped;
escaped.reserve(value.size());
for (size_t i = 0; i < value.size(); i++)
{
if (value[i] == '*')
{
escaped += "%";
}
else if (value[i] == '?')
{
escaped += "_";
}
else if (value[i] == '%')
{
escaped += "\\%";
}
else if (value[i] == '_')
{
escaped += "\\_";
}
else if (value[i] == '\\')
{
escaped += "\\\\";
}
else if (escapeBrackets && value[i] == '[')
{
escaped += "\\[";
}
else if (escapeBrackets && value[i] == ']')
{
escaped += "\\]";
}
else
{
escaped += value[i];
}
}
std::string parameter = formatter.GenerateParameter(escaped);
if (constraint.IsCaseSensitive())
{
comparison = " AND value LIKE " + parameter + " " + formatter.FormatWildcardEscape();
}
else
{
comparison = " AND lower(value) LIKE lower(" + parameter + ") " + formatter.FormatWildcardEscape();
}
}
break;
}
default:
return false;
}
if (constraint.IsMandatory())
{
target = tagFilter + comparison;
}
else if (comparison.empty())
{
target = tagFilter + " AND value IS NULL";
}
else
{
target = tagFilter + " AND value IS NULL OR " + comparison;
}
return true;
}
void ISqlLookupFormatter::GetLookupLevels(ResourceType& lowerLevel,
ResourceType& upperLevel,
const ResourceType& queryLevel,
const DatabaseDicomTagConstraints& lookup)
{
assert(ResourceType_Patient < ResourceType_Study &&
ResourceType_Study < ResourceType_Series &&
ResourceType_Series < ResourceType_Instance);
lowerLevel = queryLevel;
upperLevel = queryLevel;
for (size_t i = 0; i < lookup.GetSize(); i++)
{
ResourceType level = lookup.GetConstraint(i).GetLevel();
if (level < upperLevel)
{
upperLevel = level;
}
if (level > lowerLevel)
{
lowerLevel = level;
}
}
}
void ISqlLookupFormatter::Apply(std::string& sql,
ISqlLookupFormatter& formatter,
const DatabaseDicomTagConstraints& lookup,
ResourceType queryLevel,
const std::set<std::string>& labels,
LabelsConstraint labelsConstraint,
size_t limit)
{
// get the limit levels of the DICOM Tags lookup
ResourceType lowerLevel, upperLevel;
GetLookupLevels(lowerLevel, upperLevel, queryLevel, lookup);
assert(upperLevel <= queryLevel &&
queryLevel <= lowerLevel);
const bool escapeBrackets = formatter.IsEscapeBrackets();
std::string joins, comparisons;
size_t count = 0;
for (size_t i = 0; i < lookup.GetSize(); i++)
{
const DatabaseDicomTagConstraint& constraint = lookup.GetConstraint(i);
std::string comparison;
if (FormatComparison(comparison, formatter, constraint, count, escapeBrackets))
{
std::string join;
FormatJoin(join, constraint, count);
joins += join;
if (!comparison.empty())
{
comparisons += " AND " + comparison;
}
count ++;
}
}
sql = ("SELECT " +
FormatLevel(queryLevel) + ".publicId, " +
FormatLevel(queryLevel) + ".internalId" +
" FROM Resources AS " + FormatLevel(queryLevel));
for (int level = queryLevel - 1; level >= upperLevel; level--)
{
sql += (" INNER JOIN Resources " +
FormatLevel(static_cast<ResourceType>(level)) + " ON " +
FormatLevel(static_cast<ResourceType>(level)) + ".internalId=" +
FormatLevel(static_cast<ResourceType>(level + 1)) + ".parentId");
}
for (int level = queryLevel + 1; level <= lowerLevel; level++)
{
sql += (" INNER JOIN Resources " +
FormatLevel(static_cast<ResourceType>(level)) + " ON " +
FormatLevel(static_cast<ResourceType>(level - 1)) + ".internalId=" +
FormatLevel(static_cast<ResourceType>(level)) + ".parentId");
}
std::list<std::string> where;
where.push_back(FormatLevel(queryLevel) + ".resourceType = " +
formatter.FormatResourceType(queryLevel) + comparisons);
if (!labels.empty())
{
/**
* "In SQL Server, NOT EXISTS and NOT IN predicates are the best
* way to search for missing values, as long as both columns in
* question are NOT NULL."
* https://explainextended.com/2009/09/15/not-in-vs-not-exists-vs-left-join-is-null-sql-server/
**/
std::list<std::string> formattedLabels;
for (std::set<std::string>::const_iterator it = labels.begin(); it != labels.end(); ++it)
{
formattedLabels.push_back(formatter.GenerateParameter(*it));
}
std::string condition;
switch (labelsConstraint)
{
case LabelsConstraint_Any:
condition = "> 0";
break;
case LabelsConstraint_All:
condition = "= " + boost::lexical_cast<std::string>(labels.size());
break;
case LabelsConstraint_None:
condition = "= 0";
break;
default:
throw OrthancException(ErrorCode_ParameterOutOfRange);
}
where.push_back("(SELECT COUNT(1) FROM Labels AS selectedLabels WHERE selectedLabels.id = " + FormatLevel(queryLevel) +
".internalId AND selectedLabels.label IN (" + Join(formattedLabels, "", ", ") + ")) " + condition);
}
sql += joins + Join(where, " WHERE ", " AND ");
if (limit != 0)
{
sql += " LIMIT " + boost::lexical_cast<std::string>(limit);
}
}
void ISqlLookupFormatter::Apply(std::string& sql,
ISqlLookupFormatter& formatter,
const FindRequest& request)
{
const bool escapeBrackets = formatter.IsEscapeBrackets();
ResourceType queryLevel = request.GetLevel();
const std::string& strQueryLevel = FormatLevel(queryLevel);
ResourceType lowerLevel, upperLevel;
GetLookupLevels(lowerLevel, upperLevel, queryLevel, request.GetDicomTagConstraints());
assert(upperLevel <= queryLevel &&
queryLevel <= lowerLevel);
std::string ordering;
std::string orderingJoins;
if (request.GetOrdering().size() > 0)
{
int counter = 0;
std::vector<std::string> orderByFields;
for (std::deque<FindRequest::Ordering*>::const_iterator it = request.GetOrdering().begin(); it != request.GetOrdering().end(); ++it)
{
std::string orderingJoin;
switch ((*it)->GetKeyType())
{
case FindRequest::KeyType_DicomTag:
FormatJoinForOrdering(orderingJoin, (*it)->GetDicomTag(), counter, request.GetLevel());
break;
case FindRequest::KeyType_Metadata:
FormatJoinForOrdering(orderingJoin, (*it)->GetMetadataType(), counter, request.GetLevel());
break;
default:
throw OrthancException(ErrorCode_InternalError);
}
orderingJoins += orderingJoin;
std::string orderByField;
#if ORTHANC_SQLITE_VERSION < 3030001
// this is a way to push NULL values at the end before "NULLS LAST" was introduced:
// first filter by 0/1 and then by the column value itself
orderByField += "order" + boost::lexical_cast<std::string>(counter) + ".value IS NULL, ";
#endif
switch ((*it)->GetCast())
{
case FindRequest::OrderingCast_Int:
orderByField += "CAST(order" + boost::lexical_cast<std::string>(counter) + ".value AS INTEGER)";
break;
case FindRequest::OrderingCast_Float:
orderByField += "CAST(order" + boost::lexical_cast<std::string>(counter) + ".value AS REAL)";
break;
default:
orderByField += "order" + boost::lexical_cast<std::string>(counter) + ".value";
}
if ((*it)->GetDirection() == FindRequest::OrderingDirection_Ascending)
{
orderByField += " ASC";
}
else
{
orderByField += " DESC";
}
orderByFields.push_back(orderByField);
++counter;
}
std::string orderByFieldsString;
Toolbox::JoinStrings(orderByFieldsString, orderByFields, ", ");
ordering = "ROW_NUMBER() OVER (ORDER BY " + orderByFieldsString;
#if ORTHANC_SQLITE_VERSION >= 3030001
ordering += " NULLS LAST";
#endif
ordering += ") AS rowNumber";
}
else
{
ordering = "ROW_NUMBER() OVER (ORDER BY " + strQueryLevel + ".publicId) AS rowNumber"; // we need a default ordering in order to make default queries repeatable when using since&limit
}
sql = ("SELECT " +
strQueryLevel + ".publicId, " +
strQueryLevel + ".internalId, " +
ordering +
" FROM Resources AS " + strQueryLevel);
std::string joins, comparisons;
// handle parent constraints
if (request.GetOrthancIdentifiers().IsDefined() && request.GetOrthancIdentifiers().DetectLevel() <= queryLevel)
{
ResourceType topParentLevel = request.GetOrthancIdentifiers().DetectLevel();
if (topParentLevel == queryLevel)
{
comparisons += " AND " + FormatLevel(topParentLevel) + ".publicId = " + formatter.GenerateParameter(request.GetOrthancIdentifiers().GetLevel(topParentLevel));
}
else
{
comparisons += " AND " + FormatLevel("parent", topParentLevel) + ".publicId = " + formatter.GenerateParameter(request.GetOrthancIdentifiers().GetLevel(topParentLevel));
for (int level = queryLevel; level > topParentLevel; level--)
{
joins += " INNER JOIN Resources " +
FormatLevel("parent", static_cast<ResourceType>(level - 1)) + " ON " +
FormatLevel("parent", static_cast<ResourceType>(level - 1)) + ".internalId = ";
if (level == queryLevel)
{
joins += FormatLevel(static_cast<ResourceType>(level)) + ".parentId";
}
else
{
joins += FormatLevel("parent", static_cast<ResourceType>(level)) + ".parentId";
}
}
}
}
size_t count = 0;
const DatabaseDicomTagConstraints& dicomTagsConstraints = request.GetDicomTagConstraints();
for (size_t i = 0; i < dicomTagsConstraints.GetSize(); i++)
{
const DatabaseDicomTagConstraint& constraint = dicomTagsConstraints.GetConstraint(i);
std::string comparison;
if (FormatComparison(comparison, formatter, constraint, count, escapeBrackets))
{
std::string join;
FormatJoin(join, constraint, count);
if (constraint.GetLevel() <= queryLevel)
{
joins += join;
}
else if (constraint.GetLevel() == queryLevel + 1 && !comparison.empty())
{
// new in v 1.12.6, the constraints on child tags are actually looking for one child with this value
comparison = " EXISTS (SELECT 1 FROM Resources AS " + FormatLevel(static_cast<ResourceType>(queryLevel + 1)) +
join +
" WHERE " + comparison + " AND " +
FormatLevel(static_cast<ResourceType>(queryLevel + 1)) + ".parentId = " + FormatLevel(static_cast<ResourceType>(queryLevel)) + ".internalId) ";
}
if (!comparison.empty())
{
comparisons += " AND " + comparison;
}
count ++;
}
}
for (std::deque<DatabaseMetadataConstraint*>::const_iterator it = request.GetMetadataConstraint().begin(); it != request.GetMetadataConstraint().end(); ++it)
{
std::string comparison;
if (FormatComparison(comparison, formatter, *(*it), count, escapeBrackets))
{
std::string join;
FormatJoin(join, *(*it), request.GetLevel(), count);
joins += join;
if (!comparison.empty())
{
comparisons += " AND " + comparison;
}
count ++;
}
}
for (int level = queryLevel - 1; level >= upperLevel; level--)
{
sql += (" INNER JOIN Resources " +
FormatLevel(static_cast<ResourceType>(level)) + " ON " +
FormatLevel(static_cast<ResourceType>(level)) + ".internalId=" +
FormatLevel(static_cast<ResourceType>(level + 1)) + ".parentId");
}
// disabled in v 1.12.6 now that the child levels are considered as "is there at least one child that meets this constraint"
// for (int level = queryLevel + 1; level <= lowerLevel; level++)
// {
// sql += (" INNER JOIN Resources " +
// FormatLevel(static_cast<ResourceType>(level)) + " ON " +
// FormatLevel(static_cast<ResourceType>(level - 1)) + ".internalId=" +
// FormatLevel(static_cast<ResourceType>(level)) + ".parentId");
// }
std::list<std::string> where;
where.push_back(strQueryLevel + ".resourceType = " +
formatter.FormatResourceType(queryLevel) + comparisons);
if (!request.GetLabels().empty())
{
/**
* "In SQL Server, NOT EXISTS and NOT IN predicates are the best
* way to search for missing values, as long as both columns in
* question are NOT NULL."
* https://explainextended.com/2009/09/15/not-in-vs-not-exists-vs-left-join-is-null-sql-server/
**/
const std::set<std::string>& labels = request.GetLabels();
std::list<std::string> formattedLabels;
for (std::set<std::string>::const_iterator it = labels.begin(); it != labels.end(); ++it)
{
formattedLabels.push_back(formatter.GenerateParameter(*it));
}
std::string condition;
switch (request.GetLabelsConstraint())
{
case LabelsConstraint_Any:
condition = "> 0";
break;
case LabelsConstraint_All:
condition = "= " + boost::lexical_cast<std::string>(labels.size());
break;
case LabelsConstraint_None:
condition = "= 0";
break;
default:
throw OrthancException(ErrorCode_ParameterOutOfRange);
}
where.push_back("(SELECT COUNT(1) FROM Labels AS selectedLabels WHERE selectedLabels.id = " + strQueryLevel +
".internalId AND selectedLabels.label IN (" + Join(formattedLabels, "", ", ") + ")) " + condition);
}
sql += joins + orderingJoins + Join(where, " WHERE ", " AND ");
if (request.HasLimits())
{
sql += formatter.FormatLimits(request.GetLimitsSince(), request.GetLimitsCount());
}
}
void ISqlLookupFormatter::ApplySingleLevel(std::string& sql,
ISqlLookupFormatter& formatter,
const DatabaseDicomTagConstraints& lookup,
ResourceType queryLevel,
const std::set<std::string>& labels,
LabelsConstraint labelsConstraint,
size_t limit
)
{
ResourceType lowerLevel, upperLevel;
GetLookupLevels(lowerLevel, upperLevel, queryLevel, lookup);
assert(upperLevel == queryLevel &&
queryLevel == lowerLevel);
const bool escapeBrackets = formatter.IsEscapeBrackets();
std::vector<std::string> mainDicomTagsComparisons, dicomIdentifiersComparisons;
for (size_t i = 0; i < lookup.GetSize(); i++)
{
const DatabaseDicomTagConstraint& constraint = lookup.GetConstraint(i);
std::string comparison;
if (FormatComparison2(comparison, formatter, constraint, escapeBrackets))
{
if (!comparison.empty())
{
if (constraint.IsIdentifier())
{
dicomIdentifiersComparisons.push_back(comparison);
}
else
{
mainDicomTagsComparisons.push_back(comparison);
}
}
}
}
sql = ("SELECT publicId, internalId "
"FROM Resources "
"WHERE resourceType = " + formatter.FormatResourceType(queryLevel)
+ " ");
if (dicomIdentifiersComparisons.size() > 0)
{
for (std::vector<std::string>::const_iterator it = dicomIdentifiersComparisons.begin(); it < dicomIdentifiersComparisons.end(); ++it)
{
sql += (" AND internalId IN (SELECT id FROM DicomIdentifiers WHERE " + *it + ") ");
}
}
if (mainDicomTagsComparisons.size() > 0)
{
for (std::vector<std::string>::const_iterator it = mainDicomTagsComparisons.begin(); it < mainDicomTagsComparisons.end(); ++it)
{
sql += (" AND internalId IN (SELECT id FROM MainDicomTags WHERE " + *it + ") ");
}
}
if (!labels.empty())
{
/**
* "In SQL Server, NOT EXISTS and NOT IN predicates are the best
* way to search for missing values, as long as both columns in
* question are NOT NULL."
* https://explainextended.com/2009/09/15/not-in-vs-not-exists-vs-left-join-is-null-sql-server/
**/
std::list<std::string> formattedLabels;
for (std::set<std::string>::const_iterator it = labels.begin(); it != labels.end(); ++it)
{
formattedLabels.push_back(formatter.GenerateParameter(*it));
}
std::string condition;
std::string inOrNotIn;
switch (labelsConstraint)
{
case LabelsConstraint_Any:
condition = "> 0";
inOrNotIn = "IN";
break;
case LabelsConstraint_All:
condition = "= " + boost::lexical_cast<std::string>(labels.size());
inOrNotIn = "IN";
break;
case LabelsConstraint_None:
condition = "> 0";
inOrNotIn = "NOT IN";
break;
default:
throw OrthancException(ErrorCode_ParameterOutOfRange);
}
sql += (" AND internalId " + inOrNotIn + " (SELECT id"
" FROM (SELECT id, COUNT(1) AS labelsCount "
"FROM Labels "
"WHERE label IN (" + Join(formattedLabels, "", ", ") + ") GROUP BY id"
") AS temp "
" WHERE labelsCount " + condition + ")");
}
if (limit != 0)
{
sql += " LIMIT " + boost::lexical_cast<std::string>(limit);
}
}
}