From 9260d0bd068f9b620c5b8ad8f78c71a2a3e65365 Mon Sep 17 00:00:00 2001 From: ronso0 Date: Sun, 8 Oct 2023 00:14:49 +0200 Subject: [PATCH] Search: [text property]:="string" finds only equal strings --- src/library/searchquery.cpp | 26 ++++++++++++++++++++------ src/library/searchquery.h | 9 ++++++++- src/library/searchqueryparser.cpp | 22 ++++++++++++++++++---- src/library/searchqueryparser.h | 3 ++- src/test/searchqueryparsertest.cpp | 27 +++++++++++++++++++++++++++ 5 files changed, 75 insertions(+), 12 deletions(-) diff --git a/src/library/searchquery.cpp b/src/library/searchquery.cpp index c2e23e5490f..942af53c2eb 100644 --- a/src/library/searchquery.cpp +++ b/src/library/searchquery.cpp @@ -168,10 +168,13 @@ QString NotNode::toSql() const { TextFilterNode::TextFilterNode(const QSqlDatabase& database, const QStringList& sqlColumns, - const QString& argument) + const QString& argument, + const StringMatch matchMode) : m_database(database), m_sqlColumns(sqlColumns), - m_argument(argument) { + m_argument(argument), + m_matchMode(matchMode) { + qRegisterMetaType("StringMatch"); mixxx::DbConnection::makeStringLatinLow(&m_argument); } @@ -184,8 +187,14 @@ bool TextFilterNode::match(const TrackPointer& pTrack) const { QString strValue = value.toString(); mixxx::DbConnection::makeStringLatinLow(&strValue); - if (strValue.contains(m_argument)) { - return true; + if (m_matchMode == StringMatch::Equals) { + if (strValue == m_argument) { + return true; + } + } else { + if (strValue.contains(m_argument)) { + return true; + } } } return false; @@ -201,8 +210,13 @@ QString TextFilterNode::toSql() const { argument.append('_'); } } - QString escapedArgument = escaper.escapeString( - kSqlLikeMatchAll + argument + kSqlLikeMatchAll); + QString escapedArgument; + if (m_matchMode == StringMatch::Equals) { + escapedArgument = escaper.escapeString(argument); + } else { + escapedArgument = escaper.escapeString( + kSqlLikeMatchAll + argument + kSqlLikeMatchAll); + } QStringList searchClauses; for (const auto& sqlColumn : m_sqlColumns) { searchClauses << QString("%1 LIKE %2").arg(sqlColumn, escapedArgument); diff --git a/src/library/searchquery.h b/src/library/searchquery.h index 9f2882f2cf0..522d092a7b9 100644 --- a/src/library/searchquery.h +++ b/src/library/searchquery.h @@ -16,6 +16,11 @@ const QString kMissingFieldSearchTerm = "\"\""; // "" searches for an empty string +enum class StringMatch { + Contains = 0, + Equals, +}; + class QueryNode { public: QueryNode(const QueryNode&) = delete; // prevent copying @@ -72,7 +77,8 @@ class TextFilterNode : public QueryNode { public: TextFilterNode(const QSqlDatabase& database, const QStringList& sqlColumns, - const QString& argument); + const QString& argument, + const StringMatch matchMode = StringMatch::Contains); bool match(const TrackPointer& pTrack) const override; QString toSql() const override; @@ -81,6 +87,7 @@ class TextFilterNode : public QueryNode { QSqlDatabase m_database; QStringList m_sqlColumns; QString m_argument; + StringMatch m_matchMode; }; class NullOrEmptyTextFilterNode : public QueryNode { diff --git a/src/library/searchqueryparser.cpp b/src/library/searchqueryparser.cpp index 032ec1c60b1..65799657e50 100644 --- a/src/library/searchqueryparser.cpp +++ b/src/library/searchqueryparser.cpp @@ -69,6 +69,8 @@ SearchQueryParser::SearchQueryParser(TrackCollection* pTrackCollection, QStringL QString("^-?(%1):(.*)$").arg(m_numericFilters.join("|"))); m_specialFilterMatcher = QRegularExpression( QString("^[~-]?(%1):(.*)$").arg(m_specialFilters.join("|"))); + + qRegisterMetaType("StringMatch"); } SearchQueryParser::~SearchQueryParser() { @@ -88,7 +90,8 @@ void SearchQueryParser::setSearchColumns(QStringList searchColumns) { } QString SearchQueryParser::getTextArgument(QString argument, - QStringList* tokens) const { + QStringList* tokens, + StringMatch* matchMode) const { // If the argument is empty, assume the user placed a space after an // advanced search command. Consume another token and treat that as the // argument. @@ -99,6 +102,11 @@ QString SearchQueryParser::getTextArgument(QString argument, } } + bool shouldMatchExactly = false; + if (argument.startsWith("=")) { + argument = argument.mid(1); + shouldMatchExactly = true; + } // Deal with quoted arguments. If this token started with a quote, then // search for the closing quote. if (argument.startsWith("\"")) { @@ -128,8 +136,12 @@ QString SearchQueryParser::getTextArgument(QString argument, // return it as "" to distinguish it from an unfinished empty string argument = kMissingFieldSearchTerm; } else { + // Found a closing quote. // Slice off the quote and everything after. argument = argument.left(quote_index); + if (matchMode != nullptr && shouldMatchExactly) { + *matchMode = StringMatch::Equals; + } } } @@ -155,8 +167,8 @@ void SearchQueryParser::parseTokens(QStringList tokens, // TODO(XXX): implement this feature. } else if (textFilterMatch.hasMatch()) { QString field = textFilterMatch.captured(1); - QString argument = getTextArgument( - textFilterMatch.captured(2), &tokens); + StringMatch matchMode = StringMatch::Contains; + QString argument = getTextArgument(textFilterMatch.captured(2), &tokens, &matchMode); if (argument == kMissingFieldSearchTerm) { qDebug() << "argument explicit empty"; @@ -176,7 +188,9 @@ void SearchQueryParser::parseTokens(QStringList tokens, } else { pNode = std::make_unique( m_pTrackCollection->database(), - m_fieldToSqlColumns[field], argument); + m_fieldToSqlColumns[field], + argument, + matchMode); } } } else if (numericFilterMatch.hasMatch()) { diff --git a/src/library/searchqueryparser.h b/src/library/searchqueryparser.h index cbfe88e8916..851267a100c 100644 --- a/src/library/searchqueryparser.h +++ b/src/library/searchqueryparser.h @@ -30,7 +30,8 @@ class SearchQueryParser { AndNode* pQuery) const; QString getTextArgument(QString argument, - QStringList* tokens) const; + QStringList* tokens, + StringMatch* matchMode = nullptr) const; TrackCollection* m_pTrackCollection; QStringList m_queryColumns; diff --git a/src/test/searchqueryparsertest.cpp b/src/test/searchqueryparsertest.cpp index 14c4071d576..42c3c7039da 100644 --- a/src/test/searchqueryparsertest.cpp +++ b/src/test/searchqueryparsertest.cpp @@ -178,6 +178,33 @@ TEST_F(SearchQueryParserTest, TextFilter) { qPrintable(pQuery->toSql())); } +TEST_F(SearchQueryParserTest, TextFilterEquals) { + m_parser.setSearchColumns({"artist", "album"}); + auto pQuery(m_parser.parseQuery("comment:=\"asdf\"", QString())); + + TrackPointer pTrack(Track::newTemporary()); + pTrack->setComment("test ASDF test"); + EXPECT_FALSE(pQuery->match(pTrack)); + pTrack->setComment("ASDF"); + EXPECT_TRUE(pQuery->match(pTrack)); + + EXPECT_STREQ( + qPrintable(QString("comment LIKE 'asdf'")), + qPrintable(pQuery->toSql())); + + // Incomplete quoting should use StringMatch::Contains, + // i.e. equal to 'comment:asdf' + pQuery = m_parser.parseQuery("comment:=\"asdf", QString()); + + TrackPointer pTrack(Track::newTemporary()); + pTrack->setComment("test ASDF test"); + EXPECT_TRUE(pQuery->match(pTrack)); + + EXPECT_STREQ( + qPrintable(QString("comment LIKE '%asdf%'")), + qPrintable(pQuery->toSql())); +} + TEST_F(SearchQueryParserTest, TextFilterEmpty) { m_parser.setSearchColumns({"artist", "album"}); // An empty argument should pass everything.