Ctrl K
ComunidadGuías…

src/database/search_handler.cc

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
/*GRB*
  Gerbera - https://gerbera.io/

  search_handler.cc - this file is part of Gerbera.

  Copyright (C) 2018-2026 Gerbera Contributors

  Gerbera is free software; you can redistribute it and/or modify
  it under the terms of the GNU General Public License version 2
  as published by the Free Software Foundation.

  Gerbera 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 Gerbera.  If not, see <http://www.gnu.org/licenses/>.

  $Id$
*/

/// @file database/search_handler.cc

#define GRB_LOG_FAC GrbLogFacility::sqldatabase
#include "search_handler.h" // API

#include "database/sql_database.h"
#include "exceptions.h"
#include "metadata/metadata_enums.h"
#include "util/grb_time.h"
#include "util/tools.h"

#include <stack>

static const std::unordered_map<std::string_view, TokenType> tokenTypes {
    { "(", TokenType::LPAREN },
    { ")", TokenType::RPAREN },
    { "*", TokenType::ASTERISK },
    { "\"", TokenType::DQUOTE },
    { "true", TokenType::BOOLVAL },
    { "false", TokenType::BOOLVAL },
    { "exists", TokenType::EXISTS },
    { "contains", TokenType::STRINGOP },
    { "doesnotcontain", TokenType::STRINGOP },
    { "derivedfrom", TokenType::STRINGOP },
    { "startswith", TokenType::STRINGOP },
    { "=", TokenType::COMPAREOP },
    { "!=", TokenType::COMPAREOP },
    { "<", TokenType::COMPAREOP },
    { "<=", TokenType::COMPAREOP },
    { ">", TokenType::COMPAREOP },
    { ">=", TokenType::COMPAREOP },
    { "and", TokenType::AND },
    { "or", TokenType::OR }
};

SearchLexer::SearchLexer(std::string input)
    : input(std::move(input))
{
}

std::unique_ptr<SearchToken> SearchLexer::nextToken()
{
    while (currentPos < input.length()) {
        char ch = input[currentPos];

        switch (ch) {
        case '(':
        case ')':
        case '*':
        case '=': {
            auto token = std::string(&ch, 1);
            TokenType tokenType = tokenTypes.at(token);
            currentPos++;
            return std::make_unique<SearchToken>(tokenType, std::move(token));
        }
        case '>':
        case '<':
        case '!':
            if (input[currentPos + 1] == '=') {
                auto token = std::string(&ch, 1);
                token.push_back('=');
                TokenType tokenType = tokenTypes.at(token);
                currentPos += 2;
                return std::make_unique<SearchToken>(tokenType, std::move(token));
            } else {
                auto token = std::string(&ch, 1);
                TokenType tokenType = tokenTypes.at(token);
                currentPos++;
                return std::make_unique<SearchToken>(tokenType, std::move(token));
            }
        case '"':
            if (!inQuotes) {
                auto token = std::string(&ch, 1);
                currentPos++;
                inQuotes = true;
                return std::make_unique<SearchToken>(TokenType::DQUOTE, std::move(token));
            } else {
                auto token = std::string(&ch, 1);
                currentPos++;
                inQuotes = false;
                return std::make_unique<SearchToken>(TokenType::DQUOTE, std::move(token));
            }
        default:
            if (inQuotes) {
                auto quotedStr = getQuotedValue(input);
                if (!quotedStr.empty())
                    return std::make_unique<SearchToken>(TokenType::ESCAPEDSTRING, std::move(quotedStr));
            }
            if (std::isspace(ch)) {
                currentPos++;
            } else {
                auto tokenStr = nextStringToken(input);
                if (!tokenStr.empty()) {
                    auto token = makeToken(tokenStr);
                    if (!token->getValue().empty())
                        return token;
                }
            }
        }
    }
    return nullptr;
}

std::string SearchLexer::getQuotedValue(const std::string& input)
{
    std::string token;
    bool escaping = false;
    while (currentPos < input.length()) {
        auto ch = input[currentPos];
        if (ch == '"' && !escaping) {
            break;
        }
        if (!escaping && ch == '\\') {
            escaping = true;
        } else {
            token.push_back(ch);
            if (ch != '\\')
                escaping = false;
        }
        currentPos++;
    }
    return token;
}

std::string SearchLexer::nextStringToken(const std::string& input)
{
    auto startPos = currentPos;
    while (currentPos < input.length()) {
        auto ch = input[currentPos];
        if (std::isalnum(ch) || ch == ':' || ch == '@' || ch == '.' || ch == '_')
            currentPos++;
        else
            break;
    }
    if (startPos == currentPos)
        throw SearchParseException(fmt::format("String expected, found '{}' instead", input[currentPos]), LINE_MESSAGE);
    return input.substr(startPos, currentPos - startPos);
}

std::unique_ptr<SearchToken> SearchLexer::makeToken(std::string tokenStr)
{
    auto itr = tokenTypes.find(toLower(tokenStr));
    if (itr != tokenTypes.end()) {
        return std::make_unique<SearchToken>(itr->second, std::move(tokenStr));
    }
    return std::make_unique<SearchToken>(TokenType::PROPERTY, std::move(tokenStr));
}

SearchParser::SearchParser(const SQLEmitter& sqlEmitter, const std::string& searchCriteria, unsigned maxParenthesisDepth)
    : lexer(std::make_unique<SearchLexer>(searchCriteria))
    , sqlEmitter(sqlEmitter)
    , maxParenthesisDepth(maxParenthesisDepth)
{
}

void SearchParser::getNextToken()
{
    currentToken = lexer->nextToken();
}

std::unique_ptr<ASTNode> SearchParser::parse()
{
    getNextToken();
    if (currentToken && currentToken->getType() == TokenType::ASTERISK)
        return std::make_unique<ASTAsterisk>(sqlEmitter, currentToken->getValue());

    return parseSearchExpression();
}

std::unique_ptr<ASTNode> SearchParser::parseSearchExpression()
{
    std::stack<std::unique_ptr<ASTNode>> nodeStack;
    std::stack<TokenType> operatorStack;
    std::unique_ptr<ASTNode> root;
    TokenType currentOperator = TokenType::INVALID;
    while (currentToken) {
        if (currentToken->getType() == TokenType::PROPERTY) {
            auto expressionNode = parseRelationshipExpression();
            if (currentOperator == TokenType::AND) {
                if (!nodeStack.top())
                    throw SearchParseException("Cannot construct ASTAndOperator without lhs", LINE_MESSAGE);
                if (!expressionNode)
                    throw SearchParseException("Cannot construct ASTAndOperator without rhs", LINE_MESSAGE);
                auto lhs = std::move(nodeStack.top());
                nodeStack.pop();
                nodeStack.push(std::make_unique<ASTAndOperator>(sqlEmitter, std::move(lhs), std::move(expressionNode)));
                operatorStack.pop();
            } else if (currentOperator == TokenType::OR) {
                if (!nodeStack.top())
                    throw SearchParseException("Cannot construct ASTOrOperator without lhs", LINE_MESSAGE);
                if (!expressionNode)
                    throw SearchParseException("Cannot construct ASTOrOperator without rhs", LINE_MESSAGE);
                auto lhs = std::move(nodeStack.top());
                nodeStack.pop();
                nodeStack.push(std::make_unique<ASTOrOperator>(sqlEmitter, std::move(lhs), std::move(expressionNode)));
                operatorStack.pop();
            } else {
                nodeStack.push(std::move(expressionNode));
            }
            currentOperator = TokenType::INVALID;
            getNextToken();
        } else if (currentToken->getType() == TokenType::LPAREN) {
            nodeStack.push(parseParenthesis());
            getNextToken();
        } else if (currentToken->getType() == TokenType::AND || currentToken->getType() == TokenType::OR) {
            currentOperator = currentToken->getType();
            operatorStack.push(currentOperator);
            getNextToken();
        }
    }

    while (!nodeStack.empty()) {
        root = std::move(nodeStack.top());
        nodeStack.pop();
        if (!operatorStack.empty()) {
            currentOperator = operatorStack.top();
            operatorStack.pop();
            if (!nodeStack.empty()) {
                auto lhs = std::move(nodeStack.top());
                nodeStack.pop();
                if (currentOperator == TokenType::AND)
                    root = std::make_unique<ASTAndOperator>(sqlEmitter, std::move(lhs), std::move(root));
                else
                    root = std::make_unique<ASTOrOperator>(sqlEmitter, std::move(lhs), std::move(root));
            } else
                throw SearchParseException("Cannot construct ASTOrOperator/ASTAndOperator without rhs", LINE_MESSAGE);
        }
    }
    return root;
}

std::unique_ptr<ASTNode> SearchParser::parseParenthesis(unsigned depth)
{
    if (depth >= maxParenthesisDepth)
        throw SearchParseException(fmt::format("Failed to parse search criteria - more than {} nested parentheses", maxParenthesisDepth), LINE_MESSAGE);

    if (currentToken->getType() != TokenType::LPAREN)
        throw SearchParseException("Failed to parse search criteria - expecting a ')'", LINE_MESSAGE);

    std::unique_ptr<ASTNode> currentNode;
    getNextToken();
    while (currentToken && currentToken->getType() != TokenType::RPAREN) {
        // just call parseSearchExpression() at this point?
        if (currentToken->getType() == TokenType::PROPERTY) {
            currentNode = parseRelationshipExpression();
            getNextToken();
        } else if (currentToken->getType() == TokenType::AND || currentToken->getType() == TokenType::OR) {
            auto tokenType = currentToken->getType();
            auto lhsNode = std::move(currentNode);

            getNextToken();
            auto rhsNode = (currentToken->getType() == TokenType::LPAREN)
                ? parseParenthesis(depth + 1)
                : parseRelationshipExpression();

            if (tokenType == TokenType::AND)
                currentNode = std::make_unique<ASTAndOperator>(sqlEmitter, std::move(lhsNode), std::move(rhsNode));
            else if (tokenType == TokenType::OR)
                currentNode = std::make_unique<ASTOrOperator>(sqlEmitter, std::move(lhsNode), std::move(rhsNode));
            else
                throw SearchParseException("Failed to parse search criteria - expected and/or", LINE_MESSAGE);

            getNextToken();
        } else if (currentToken->getType() == TokenType::LPAREN) {
            currentNode = parseParenthesis(depth + 1);
            getNextToken();
        }
    }
    if (!currentNode)
        throw SearchParseException("Failed to parse search criteria - bad expression between parenthesis", LINE_MESSAGE);

    return std::make_unique<ASTParenthesis>(sqlEmitter, std::move(currentNode));
}

std::unique_ptr<ASTNode> SearchParser::parseRelationshipExpression()
{
    if (!currentToken || currentToken->getType() != TokenType::PROPERTY)
        throw_std_runtime_error("Failed to parse search criteria - expecting a property name");

    auto property = std::make_unique<ASTProperty>(sqlEmitter, currentToken->getValue());

    getNextToken();
    if (currentToken && currentToken->getType() == TokenType::COMPAREOP) {
        auto operatr = std::make_unique<ASTCompareOperator>(sqlEmitter, currentToken->getValue());
        getNextToken();
        auto quotedString = parseQuotedString();
        return std::make_unique<ASTCompareExpression>(sqlEmitter, std::move(property), std::move(operatr), std::move(quotedString));
    }

    if (currentToken && currentToken->getType() == TokenType::STRINGOP) {
        auto operatr = std::make_unique<ASTStringOperator>(sqlEmitter, currentToken->getValue());
        getNextToken();
        auto quotedString = parseQuotedString();
        return std::make_unique<ASTStringExpression>(sqlEmitter, std::move(property), std::move(operatr), std::move(quotedString));
    }

    if (currentToken && currentToken->getType() == TokenType::EXISTS) {
        auto operatr = std::make_unique<ASTExistsOperator>(sqlEmitter, currentToken->getValue());
        getNextToken();
        auto booleanValue = std::make_unique<ASTBoolean>(sqlEmitter, currentToken->getValue());
        return std::make_unique<ASTExistsExpression>(sqlEmitter, std::move(property), std::move(operatr), std::move(booleanValue));
    }

    throw SearchParseException("Failed to parse search criteria - expecting a comparison, exists, or string operator", LINE_MESSAGE);
}

std::unique_ptr<ASTQuotedString> SearchParser::parseQuotedString()
{
    if (!currentToken || currentToken->getType() != TokenType::DQUOTE)
        throw_std_runtime_error("Failed to parse search criteria - expecting a double-quote");
    auto openQuote = std::make_unique<ASTDQuote>(sqlEmitter, currentToken->getValue());
    getNextToken();

    if (!currentToken || (currentToken->getType() != TokenType::ESCAPEDSTRING && currentToken->getType() != TokenType::DQUOTE)) {
        throw SearchParseException("Failed to parse search criteria - expecting an escaped string value", LINE_MESSAGE);
    }

    auto escapedString = currentToken->getType() != TokenType::DQUOTE ? std::make_unique<ASTEscapedString>(sqlEmitter, currentToken->getValue()) : std::make_unique<ASTEscapedString>(sqlEmitter, "");
    if (currentToken && currentToken->getType() != TokenType::DQUOTE)
        getNextToken();

    if (!currentToken || currentToken->getType() != TokenType::DQUOTE)
        throw SearchParseException("Failed to parse search criteria - expecting a double-quote", LINE_MESSAGE);
    auto closeQuote = std::make_unique<ASTDQuote>(sqlEmitter, currentToken->getValue());

    return std::make_unique<ASTQuotedString>(sqlEmitter, std::move(openQuote), std::move(escapedString), std::move(closeQuote));
}

std::pair<std::string, std::string> ASTNode::emitSQL() const
{
    return sqlEmitter.emitSQL(this);
}

ASTAsterisk::ASTAsterisk(const SQLEmitter& sqlEmitter, std::string value)
    : ASTNode(sqlEmitter)
    , value(std::move(value))
{
}

std::string ASTAsterisk::emit() const
{
    return sqlEmitter.emit(this);
}

ASTProperty::ASTProperty(const SQLEmitter& sqlEmitter, std::string value)
    : ASTNode(sqlEmitter)
    , value(std::move(value))
{
}

std::string ASTProperty::emit() const
{
    return value;
}

ASTBoolean::ASTBoolean(const SQLEmitter& sqlEmitter, std::string value)
    : ASTNode(sqlEmitter)
    , value(std::move(value))
{
}

std::string ASTBoolean::emit() const
{
    return value;
}

ASTParenthesis::ASTParenthesis(const SQLEmitter& sqlEmitter, std::unique_ptr<ASTNode> node)
    : ASTNode(sqlEmitter)
    , bracketedNode(std::move(node))
{
}

std::string ASTParenthesis::emit() const
{
    return sqlEmitter.emit(this, bracketedNode->emit());
}

ASTDQuote::ASTDQuote(const SQLEmitter& sqlEmitter, std::string value)
    : ASTNode(sqlEmitter)
    , value(std::move(value))
{
}

std::string ASTDQuote::emit() const
{
    return sqlEmitter.emit(this);
}

ASTEscapedString::ASTEscapedString(const SQLEmitter& sqlEmitter, std::string value)
    : ASTNode(sqlEmitter)
    , value(std::move(value))
{
}

std::string ASTEscapedString::emit() const
{
    return value;
}

ASTQuotedString::ASTQuotedString(const SQLEmitter& sqlEmitter, std::unique_ptr<ASTDQuote> openQuote,
    std::unique_ptr<ASTEscapedString> escapedString, std::unique_ptr<ASTDQuote> closeQuote)
    : ASTNode(sqlEmitter)
    , openQuote(std::move(openQuote))
    , escapedString(std::move(escapedString))
    , closeQuote(std::move(closeQuote))
{
}

std::string ASTQuotedString::emit() const
{
    return openQuote->emit() + escapedString->emit() + closeQuote->emit();
}

ASTCompareOperator::ASTCompareOperator(const SQLEmitter& sqlEmitter, std::string value)
    : ASTNode(sqlEmitter)
    , value(std::move(value))
{
}

std::string ASTCompareOperator::emit() const
{
    log_error("Emitting for ASTCompareOperator");
    throw_std_runtime_error("Should not get here");
}

std::string ASTCompareOperator::emit(const std::string& property, const std::string& value) const
{
    return sqlEmitter.emit(this, property, value);
}

ASTCompareExpression::ASTCompareExpression(const SQLEmitter& sqlEmitter, std::unique_ptr<ASTProperty> lhs,
    std::unique_ptr<ASTCompareOperator> operatr, std::unique_ptr<ASTQuotedString> rhs)
    : ASTNode(sqlEmitter)
    , lhs(std::move(lhs))
    , operatr(std::move(operatr))
    , rhs(std::move(rhs))
{
}

std::string ASTCompareExpression::emit() const
{
    return operatr->emit(lhs->emit(), rhs->emit());
}

ASTStringOperator::ASTStringOperator(const SQLEmitter& sqlEmitter, std::string value)
    : ASTNode(sqlEmitter)
    , value(std::move(value))
{
}

std::string ASTStringOperator::emit() const
{
    log_error("Emitting for ASTStringOperator");
    throw_std_runtime_error("Should not get here");
}

std::string ASTStringOperator::emit(const std::string& property, const std::string& value) const
{
    return sqlEmitter.emit(this, property, value);
}

ASTStringExpression::ASTStringExpression(const SQLEmitter& sqlEmitter, std::unique_ptr<ASTProperty> lhs,
    std::unique_ptr<ASTStringOperator> operatr, std::unique_ptr<ASTQuotedString> rhs)
    : ASTNode(sqlEmitter)
    , lhs(std::move(lhs))
    , operatr(std::move(operatr))
    , rhs(std::move(rhs))
{
}

std::string ASTStringExpression::emit() const
{
    return operatr->emit(lhs->emit(), rhs->emit());
}

ASTExistsOperator::ASTExistsOperator(const SQLEmitter& sqlEmitter, std::string value)
    : ASTNode(sqlEmitter)
    , value(std::move(value))
{
}

std::string ASTExistsOperator::emit() const
{
    log_error("Emitting for ASTExistsOperator");
    throw_std_runtime_error("Should not get here");
}

std::string ASTExistsOperator::emit(const std::string& property, const std::string& value) const
{
    return sqlEmitter.emit(this, property, value);
}

ASTExistsExpression::ASTExistsExpression(const SQLEmitter& sqlEmitter, std::unique_ptr<ASTProperty> lhs,
    std::unique_ptr<ASTExistsOperator> operatr, std::unique_ptr<ASTBoolean> rhs)
    : ASTNode(sqlEmitter)
    , lhs(std::move(lhs))
    , operatr(std::move(operatr))
    , rhs(std::move(rhs))
{
}

std::string ASTExistsExpression::emit() const
{
    return operatr->emit(lhs->emit(), rhs->emit());
}

ASTAndOperator::ASTAndOperator(const SQLEmitter& sqlEmitter, std::unique_ptr<ASTNode> lhs, std::unique_ptr<ASTNode> rhs)
    : ASTNode(sqlEmitter)
    , lhs(std::move(lhs))
    , rhs(std::move(rhs))
{
}
std::string ASTAndOperator::emit() const
{
    return sqlEmitter.emit(this, lhs->emit(), rhs->emit());
}

ASTOrOperator::ASTOrOperator(const SQLEmitter& sqlEmitter, std::unique_ptr<ASTNode> lhs, std::unique_ptr<ASTNode> rhs)
    : ASTNode(sqlEmitter)
    , lhs(std::move(lhs))
    , rhs(std::move(rhs))
{
}

std::string ASTOrOperator::emit() const
{
    return sqlEmitter.emit(this, lhs->emit(), rhs->emit());
}

DefaultSQLEmitter::DefaultSQLEmitter(
    std::shared_ptr<Database> database,
    std::shared_ptr<ColumnMapper> colMapper,
    std::shared_ptr<ColumnMapper> metaMapper,
    std::shared_ptr<ColumnMapper> resMapper,
    std::shared_ptr<ColumnMapper> plyMapper)
    : database(std::move(database))
    , colMapper(std::move(colMapper))
    , metaMapper(std::move(metaMapper))
    , resMapper(std::move(resMapper))
    , plyMapper(std::move(plyMapper))
{
}

std::pair<std::string, std::string> DefaultSQLEmitter::emitSQL(const ASTNode* node) const
{
    metaMapper->resetCnt();
    std::string predicates = node->emit();
    if (!predicates.empty()) {
        return { predicates, metaMapper->getJoin() };
    }
    throw_std_runtime_error("No SQL generated from AST");
}

std::string DefaultSQLEmitter::emit(const ASTParenthesis* node, const std::string& bracketedNode) const
{
    return fmt::format("({})", bracketedNode);
}

// format indexes:
//    0: class statement
//    1: property name statement
//    2: property statement lower case
//    3: value
//    4: property type
static const std::map<std::string, std::string> logicOperator {
    { "contains", "({2} LIKE LOWER({3}))" }, // lower
    { "doesnotcontain", "({2} NOT LIKE LOWER({3}))" }, // lower
    { "startswith", "({2} LIKE LOWER({3}))" }, // lower
    { "derivedfrom", "({2} LIKE LOWER({3}))" },
    { "exists", "({1} IS {3})" },
    { "@exists", "({1} IS {3})" },
    { "newer", "({1} {3})" },
    { "compare", "({2}LOWER({3}))" }, // lower
    { "@compare", "({2}LOWER({3}))" }, // lower
    { "neq", "({2}LOWER({3}))" }, // lower
    { "@neq", "({2}LOWER({3}))" }, // lower
};
static const std::map<std::string, std::string> valueOperator {
    { "contains", "%{}%" },
    { "doesnotcontain", "%{}%" },
    { "startswith", "{}%" },
    { "derivedfrom", "{}%" },
    { "exists", "{}" },
    { "@exists", "{}" },
    { "newer", "{}" },
    { "compare", "{}" },
    { "@compare", "{}" },
    { "neq", "{}" },
    { "@neq", "{}" },
};

std::tuple<std::string, std::string, FieldType> DefaultSQLEmitter::getPropertyStatement(const std::string& property) const
{
    if (colMapper && colMapper->hasEntry(property)) {
        return { colMapper->mapQuoted(property), colMapper->mapQuotedLower(property), colMapper->getFieldType(property) };
    }
    if (resMapper && resMapper->hasEntry(property)) {
        return { resMapper->mapQuoted(property), resMapper->mapQuotedLower(property), resMapper->getFieldType(property) };
    }
    if (plyMapper && plyMapper->hasEntry(property)) {
        return { plyMapper->mapQuoted(property), plyMapper->mapQuotedLower(property), plyMapper->getFieldType(property) };
    }
    if (metaMapper) {
        metaMapper->setAlias("meta_query");
        metaMapper->addJoin(fmt::format("LEFT JOIN {} ON {} = {}",
            metaMapper->tableQuoted(),
            colMapper->mapQuoted(UPNP_SEARCH_ID), metaMapper->mapQuotedTable(UPNP_SEARCH_ID)));
        return {
            fmt::format("{0}='{2}' AND {1}", metaMapper->mapQuotedTable(META_NAME), metaMapper->mapQuotedTable(META_VALUE), property),
            fmt::format("{0}='{2}' AND {1}", metaMapper->mapQuotedTable(META_NAME), metaMapper->mapQuotedTableLower(META_VALUE), property),
            metaMapper->getFieldType(property)
        };
    }
    log_warning("Property {} not yet supported. Search may return no result!", property);
    return {};
}

std::string DefaultSQLEmitter::emit(
    const ASTCompareOperator* node,
    const std::string& property,
    const std::string& value) const
{
    auto operatr = node->getValue();
    auto [prpUpper, prpLower, prpType] = getPropertyStatement(property);

    if (prpType == FieldType::Date) {
        if ((operatr == ">" || operatr == ">=") && startswith(value, "@last")) {
            auto dateVal = currentTime() - std::chrono::hours(24 * stoiString(value.substr(5)));
            return fmt::format(logicOperator.at("newer"),
                "",
                fmt::format("{} {}", prpUpper, operatr),
                fmt::format("{} {}", prpLower, operatr),
                dateVal.count(),
                prpType);
        }
        if (operatr == ">" || operatr == ">=" || operatr == "<" || operatr == "<=") {
            std::chrono::seconds dateVal(0);
            if (parseSimpleDate(value, dateVal)) {
                log_debug("parseSimpleDate {} -> {}", value, dateVal.count());
                return fmt::format(logicOperator.at("newer"),
                    "",
                    fmt::format("{} {}", prpUpper, operatr),
                    fmt::format("{} {}", prpLower, operatr),
                    dateVal.count(),
                    prpType);
            }
        }
    }
    if (prpType == FieldType::Integer && (operatr == ">" || operatr == ">=" || operatr == "<" || operatr == "<=")) {
        return fmt::format("{} {} {}", prpUpper, operatr, stoiString(value));
    }

    auto cmpOp = (property[0] == '@') ? "@compare" : "compare";
    if (operatr == "!=")
        cmpOp = (property[0] == '@') ? "@neq" : "neq";
    else if (operatr != "=")
        throw_std_runtime_error("Operation '{}' '{}' not yet supported", operatr, value);

    auto clsUpper = std::get<0>(getPropertyStatement(UPNP_SEARCH_CLASS));
    return fmt::format(logicOperator.at(cmpOp),
        clsUpper,
        fmt::format("{}{}", prpUpper, operatr),
        fmt::format("{}{}", prpLower, operatr),
        database->quote(fmt::format(valueOperator.at(cmpOp), value)),
        prpType);
}

std::string DefaultSQLEmitter::emit(
    const ASTStringOperator* node,
    const std::string& property,
    const std::string& value) const
{
    auto stringOperator = toLower(node->getValue());
    if (logicOperator.find(stringOperator) == logicOperator.end()) {
        throw SearchParseException(fmt::format("Operation '{}' not yet supported", stringOperator), LINE_MESSAGE);
    }
    auto [prpUpper, prpLower, prpType] = getPropertyStatement(property);
    auto clsUpper = std::get<0>(getPropertyStatement(UPNP_SEARCH_CLASS));
    return fmt::format(logicOperator.at(stringOperator),
        clsUpper,
        prpUpper,
        prpLower,
        database->quote(fmt::format(valueOperator.at(stringOperator), value)),
        prpType);
}

std::string DefaultSQLEmitter::emit(
    const ASTExistsOperator* node,
    const std::string& property,
    const std::string& value) const
{
    std::string exists;
    if (value == "true") {
        exists = "NOT NULL";
    } else if (value == "false") {
        exists = "NULL";
    } else {
        throw SearchParseException(fmt::format("Invalid value '{}' on rhs of 'exists' operator", value), LINE_MESSAGE);
    }
    auto [prpUpper, prpLower, prpType] = getPropertyStatement(property);
    auto clsUpper = std::get<0>(getPropertyStatement(UPNP_SEARCH_CLASS));
    return fmt::format(logicOperator.at((property[0] == '@') ? "@exists" : "exists"),
        clsUpper,
        prpUpper,
        prpLower,
        exists,
        prpType);
}

std::string DefaultSQLEmitter::emit(const ASTAndOperator* node, const std::string& lhs,
    const std::string& rhs) const
{
    return fmt::format("{} AND {}", lhs, rhs);
}

std::string DefaultSQLEmitter::emit(const ASTOrOperator* node, const std::string& lhs,
    const std::string& rhs) const
{
    return fmt::format("{} OR {}", lhs, rhs);
}

SortParser::SortParser(std::shared_ptr<ColumnMapper> colMapper, std::shared_ptr<ColumnMapper> plyMapper, std::shared_ptr<ColumnMapper> metaMapper, std::string sortCriteria)
    : colMapper(std::move(colMapper))
    , plyMapper(std::move(plyMapper))
    , metaMapper(std::move(metaMapper))
    , sortCrit(std::move(sortCriteria))
{
}

std::string SortParser::parse(std::string& addColumns, std::string& addJoin)
{
    if (sortCrit.empty() || !colMapper || !metaMapper)
        return {};

    std::vector<std::string> sort;
    std::vector<std::string> colBuf;
    std::vector<std::string> joinBuf;
    size_t cnt = 0;
    for (auto&& seg : splitString(sortCrit, ',')) {
        trimStringInPlace(seg);
        bool desc = (seg[0] == '-');
        if (seg[0] == '-' || seg[0] == '+') {
            seg = seg.substr(1);
        } else {
            log_warning("Unknown sort direction '{}' in '{}'", seg, sortCrit);
        }
        auto isCol = colMapper->mapQuotedList(sort, seg, (desc ? "DESC" : "ASC"));
        auto isPly = plyMapper->mapQuotedList(sort, seg, (desc ? "DESC" : "ASC"));
        if (!isCol && !isPly) {
            std::string sortSql;
            for (auto&& metaId : MetadataIterator()) {
                auto&& metaName = MetaEnumMapper::getMetaFieldName(metaId);
                if (metaName == seg) {
                    sortSql = metaName;
                }
            }
            if (!sortSql.empty()) {
                log_debug("Sort by meta data '{}'", sortSql);
                auto metaAlias = fmt::format("meta_prop{}", cnt);
                colBuf.push_back(fmt::format("{}.{}", metaMapper->quote(metaAlias), metaMapper->mapQuoted(META_VALUE, true)));
                joinBuf.push_back(fmt::format("LEFT JOIN {0} {1} ON {2} = {1}.{3} AND {1}.{4} = '{5}'",
                    metaMapper->getTableName(), metaMapper->quote(metaAlias),
                    colMapper->mapQuoted(UPNP_SEARCH_ID),
                    metaMapper->mapQuoted(UPNP_SEARCH_ID, true),
                    metaMapper->mapQuoted(META_NAME, true),
                    sortSql));

                sort.push_back(fmt::format("{}.{} {}", metaMapper->quote(metaAlias), metaMapper->mapQuoted(META_VALUE, true), (desc ? "DESC" : "ASC")));
                cnt++;
            } else {
                log_warning("Unknown sort key '{}' in '{}'", seg, sortCrit);
            }
        } else if (isPly) {
            colBuf.push_back(plyMapper->mapQuoted(seg));
        }
    }
    if (!colBuf.empty()) {
        colBuf.emplace(colBuf.begin(), ""); // result starts with ,
    }
    addColumns = fmt::format("{}", fmt::join(colBuf, ", "));
    addJoin = fmt::format("{}", fmt::join(joinBuf, " "));
    return fmt::format("{}", fmt::join(sort, ", "));
}