Skip to content

Commit

Permalink
Search: [text property]:="string" finds only equal strings
Browse files Browse the repository at this point in the history
  • Loading branch information
ronso0 committed Oct 8, 2023
1 parent 17362a7 commit 2d8d1fc
Show file tree
Hide file tree
Showing 5 changed files with 60 additions and 12 deletions.
22 changes: 16 additions & 6 deletions src/library/searchquery.cpp
Expand Up @@ -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>("StringMatch");
mixxx::DbConnection::makeStringLatinLow(&m_argument);
}

Expand All @@ -184,8 +187,10 @@ 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) {
return strValue == m_argument;
} else {
return strValue.contains(m_argument);
}
}
return false;
Expand All @@ -201,8 +206,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);
Expand Down
9 changes: 8 additions & 1 deletion src/library/searchquery.h
Expand Up @@ -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
Expand Down Expand Up @@ -72,7 +77,8 @@ class TextFilterNode : public QueryNode {
public:
TextFilterNode(const QSqlDatabase& database,
const QStringList& sqlColumns,
const QString& argument);
const QString& argument,
const StringMatch match = StringMatch::Contains);

bool match(const TrackPointer& pTrack) const override;
QString toSql() const override;
Expand All @@ -81,6 +87,7 @@ class TextFilterNode : public QueryNode {
QSqlDatabase m_database;
QStringList m_sqlColumns;
QString m_argument;
StringMatch m_matchMode;
};

class NullOrEmptyTextFilterNode : public QueryNode {
Expand Down
22 changes: 18 additions & 4 deletions src/library/searchqueryparser.cpp
Expand Up @@ -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>("StringMatch");
}

SearchQueryParser::~SearchQueryParser() {
Expand All @@ -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.
Expand All @@ -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("\"")) {
Expand Down Expand Up @@ -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;
}
}
}

Expand All @@ -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";
Expand All @@ -176,7 +188,9 @@ void SearchQueryParser::parseTokens(QStringList tokens,
} else {
pNode = std::make_unique<TextFilterNode>(
m_pTrackCollection->database(),
m_fieldToSqlColumns[field], argument);
m_fieldToSqlColumns[field],
argument,
matchMode);
}
}
} else if (numericFilterMatch.hasMatch()) {
Expand Down
3 changes: 2 additions & 1 deletion src/library/searchqueryparser.h
Expand Up @@ -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;
Expand Down
16 changes: 16 additions & 0 deletions src/test/searchqueryparsertest.cpp
Expand Up @@ -178,6 +178,22 @@ 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()));
}

TEST_F(SearchQueryParserTest, TextFilterEmpty) {
m_parser.setSearchColumns({"artist", "album"});
// An empty argument should pass everything.
Expand Down

0 comments on commit 2d8d1fc

Please sign in to comment.