diff --git a/autotests/unit/lib/advancedqueryparsertest.cpp b/autotests/unit/lib/advancedqueryparsertest.cpp index fa4709bb..7b4a5bbb 100644 --- a/autotests/unit/lib/advancedqueryparsertest.cpp +++ b/autotests/unit/lib/advancedqueryparsertest.cpp @@ -1,332 +1,331 @@ /* * This file is part of the KDE Baloo Project * Copyright (C) 2014 Vishesh Handa * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * */ #include "advancedqueryparser.h" #include Q_DECLARE_METATYPE(Baloo::Term) using namespace Baloo; class AdvancedQueryParserTest : public QObject { Q_OBJECT private Q_SLOTS: void testSimpleProperty(); void testSimpleString(); void testStringAndProperty(); void testLogicalOps(); void testNesting(); void testDateTime(); void testOperators(); void testBinaryOperatorMissingFirstArg(); void testNestedParentheses(); void testNestedParentheses_data(); void testOptimizedLogic(); void testOptimizedLogic_data(); }; void AdvancedQueryParserTest::testSimpleProperty() { AdvancedQueryParser parser; Term term = parser.parse(QStringLiteral("artist:Coldplay")); Term expectedTerm(QStringLiteral("artist"), "Coldplay"); QCOMPARE(term, expectedTerm); } void AdvancedQueryParserTest::testSimpleString() { AdvancedQueryParser parser; Term term = parser.parse(QStringLiteral("Coldplay")); Term expectedTerm(QLatin1String(""), "Coldplay"); QCOMPARE(term, expectedTerm); } void AdvancedQueryParserTest::testStringAndProperty() { AdvancedQueryParser parser; Term term = parser.parse(QStringLiteral("stars artist:Coldplay fire")); Term expectedTerm(Term::And); expectedTerm.addSubTerm(Term(QLatin1String(""), "stars")); expectedTerm.addSubTerm(Term(QStringLiteral("artist"), "Coldplay")); expectedTerm.addSubTerm(Term(QLatin1String(""), "fire")); QCOMPARE(term, expectedTerm); } void AdvancedQueryParserTest::testLogicalOps() { // AND AdvancedQueryParser parser; Term term = parser.parse(QStringLiteral("artist:Coldplay AND type:song")); Term expectedTerm(Term::And); expectedTerm.addSubTerm(Term(QStringLiteral("artist"), "Coldplay")); expectedTerm.addSubTerm(Term(QStringLiteral("type"), "song")); QCOMPARE(term, expectedTerm); // OR term = parser.parse(QStringLiteral("artist:Coldplay OR type:song")); expectedTerm = Term(Term::Or); expectedTerm.addSubTerm(Term(QStringLiteral("artist"), "Coldplay")); expectedTerm.addSubTerm(Term(QStringLiteral("type"), "song")); QCOMPARE(term, expectedTerm); // AND then OR term = parser.parse(QStringLiteral("artist:Coldplay AND type:song OR stars")); expectedTerm = Term(Term::Or); expectedTerm.addSubTerm(Term(QStringLiteral("artist"), "Coldplay") && Term(QStringLiteral("type"), "song")); expectedTerm.addSubTerm(Term(QLatin1String(""), "stars")); QCOMPARE(term, expectedTerm); // OR then AND term = parser.parse(QStringLiteral("artist:Coldplay OR type:song AND stars")); expectedTerm = Term(Term::And); expectedTerm.addSubTerm(Term(QStringLiteral("artist"), "Coldplay") || Term(QStringLiteral("type"), "song")); expectedTerm.addSubTerm(Term(QLatin1String(""), "stars")); QCOMPARE(term, expectedTerm); // Multiple ANDs term = parser.parse(QStringLiteral("artist:Coldplay AND type:song AND stars")); expectedTerm = Term(Term::And); expectedTerm.addSubTerm(Term(QStringLiteral("artist"), "Coldplay")); expectedTerm.addSubTerm(Term(QStringLiteral("type"), "song")); expectedTerm.addSubTerm(Term(QLatin1String(""), "stars")); QCOMPARE(term, expectedTerm); // Multiple ORs term = parser.parse(QStringLiteral("artist:Coldplay OR type:song OR stars")); expectedTerm = Term(Term::Or); expectedTerm.addSubTerm(Term(QStringLiteral("artist"), "Coldplay")); expectedTerm.addSubTerm(Term(QStringLiteral("type"), "song")); expectedTerm.addSubTerm(Term(QLatin1String(""), "stars")); QCOMPARE(term, expectedTerm); } void AdvancedQueryParserTest::testNesting() { AdvancedQueryParser parser; Term term = parser.parse(QStringLiteral("artist:Coldplay AND (type:song OR stars) fire")); Term expectedTerm(Term::And); expectedTerm.addSubTerm(Term(QStringLiteral("artist"), "Coldplay")); expectedTerm.addSubTerm(Term(QStringLiteral("type"), "song") || Term(QLatin1String(""), "stars")); expectedTerm.addSubTerm(Term(QLatin1String(""), "fire")); QCOMPARE(term, expectedTerm); } void AdvancedQueryParserTest::testDateTime() { // Integers AdvancedQueryParser parser; Term term; Term expectedTerm; term = parser.parse(QStringLiteral("modified:2014-12-02")); expectedTerm = Term(QStringLiteral("modified"), QDate(2014, 12, 02)); QCOMPARE(term, expectedTerm); term = parser.parse(QStringLiteral("modified:\"2014-12-02T23:22:1\"")); expectedTerm = Term(QStringLiteral("modified"), QDateTime(QDate(2014, 12, 02), QTime(23, 22, 1))); QEXPECT_FAIL("", "AQP cannot handle datetime", Abort); QCOMPARE(term, expectedTerm); } void AdvancedQueryParserTest::testOperators() { AdvancedQueryParser parser; Term term; Term expectedTerm; term = parser.parse(QStringLiteral("width:500")); expectedTerm = Term(QStringLiteral("width"), 500, Term::Equal); QCOMPARE(term, expectedTerm); term = parser.parse(QStringLiteral("width=500")); expectedTerm = Term(QStringLiteral("width"), 500, Term::Equal); QCOMPARE(term, expectedTerm); term = parser.parse(QStringLiteral("width<500")); expectedTerm = Term(QStringLiteral("width"), 500, Term::Less); QCOMPARE(term, expectedTerm); term = parser.parse(QStringLiteral("width<=500")); expectedTerm = Term(QStringLiteral("width"), 500, Term::LessEqual); QCOMPARE(term, expectedTerm); term = parser.parse(QStringLiteral("width>500")); expectedTerm = Term(QStringLiteral("width"), 500, Term::Greater); QCOMPARE(term, expectedTerm); term = parser.parse(QStringLiteral("width>=500")); expectedTerm = Term(QStringLiteral("width"), 500, Term::GreaterEqual); QCOMPARE(term, expectedTerm); } void AdvancedQueryParserTest::testBinaryOperatorMissingFirstArg() { AdvancedQueryParser parser; Term term = parser.parse(QStringLiteral("=:2")); Term expectedTerm; QCOMPARE(term, expectedTerm); } void AdvancedQueryParserTest::testNestedParentheses() { QFETCH(QString, searchInput); QFETCH(QString, failmessage); QFETCH(Term, expectedTerm); AdvancedQueryParser parser; const auto testTerm = parser.parse(searchInput); qDebug() << " result term" << testTerm; qDebug() << "expected term" << expectedTerm; if (!failmessage.isEmpty()) { QEXPECT_FAIL("", qPrintable(failmessage), Continue); } QCOMPARE(testTerm, expectedTerm); } void AdvancedQueryParserTest::testNestedParentheses_data() { QTest::addColumn("searchInput"); QTest::addColumn("expectedTerm"); QTest::addColumn("failmessage"); QTest::newRow("a AND b AND c AND d") << QStringLiteral("a AND b AND c AND d") << Term{Term::And, QList{ Term{QString(), QStringLiteral("a"), Term::Contains}, Term{QString(), QStringLiteral("b"), Term::Contains}, Term{QString(), QStringLiteral("c"), Term::Contains}, Term{QString(), QStringLiteral("d"), Term::Contains}, }} << QString() ; QTest::newRow("(a AND b) AND (c OR d)") << QStringLiteral("(a AND b) AND (c OR d)") << Term{Term::And, QList{ Term{QString(), QStringLiteral("a"), Term::Contains}, Term{QString(), QStringLiteral("b"), Term::Contains}, Term{Term::Or, QList{ Term{QString(), QStringLiteral("c"), Term::Contains}, Term{QString(), QStringLiteral("d"), Term::Contains}, }} }} << QString() ; QTest::newRow("(a AND (b AND (c AND d)))") << QStringLiteral("(a AND (b AND (c AND d)))") << Term{Term::And, QList{ Term{QString(), QStringLiteral("a"), Term::Contains}, Term{QString(), QStringLiteral("b"), Term::Contains}, Term{QString(), QStringLiteral("c"), Term::Contains}, Term{QString(), QStringLiteral("d"), Term::Contains}, }} << QStringLiteral("Fails to optimize for unknown reason, but output is semantically correct") ; // This test verifies that the above test is semantically correct QTest::newRow("(a AND (b AND (c AND d))) semantic") << QStringLiteral("(a AND (b AND (c AND d)))") << Term{Term::And, QList{ Term{QString(), QStringLiteral("a"), Term::Contains}, Term{Term::And, QList{ Term{QString(), QStringLiteral("b"), Term::Contains}, Term{Term::And, QList{ Term{QString(), QStringLiteral("c"), Term::Contains}, Term{QString(), QStringLiteral("d"), Term::Contains} }} }} }} << QString() ; } void AdvancedQueryParserTest::testOptimizedLogic() { QFETCH(Term, testTerm); QFETCH(Term, expectedTerm); qDebug() << " result term" << testTerm; qDebug() << "expected term" << expectedTerm; - QEXPECT_FAIL("", "no optimization", Continue); QCOMPARE(testTerm, expectedTerm); } void AdvancedQueryParserTest::testOptimizedLogic_data() { QTest::addColumn("testTerm"); QTest::addColumn("expectedTerm"); // a && b && c && d can be combined into one AND term with 4 subterms QTest::addRow("a && b && c && d") << (Term{QString(), QStringLiteral("a"), Term::Contains} && Term{QString(), QStringLiteral("b"), Term::Contains} && Term{QString(), QStringLiteral("c"), Term::Contains} && Term{QString(), QStringLiteral("d"), Term::Contains}) << Term{Term::And, QList{ Term{QString(), QStringLiteral("a"), Term::Contains}, Term{QString(), QStringLiteral("b"), Term::Contains}, Term{QString(), QStringLiteral("c"), Term::Contains}, Term{QString(), QStringLiteral("d"), Term::Contains}, }} ; // (a AND b) AND (c OR d) can be merged as (a AND b AND (c OR D) QTest::addRow("(a && b) && (c || d)") << ((Term{QString(), QStringLiteral("a"), Term::Contains} && Term{QString(), QStringLiteral("b"), Term::Contains}) && (Term{QString(), QStringLiteral("c"), Term::Contains} || Term{QString(), QStringLiteral("d"), Term::Contains} )) << Term{Term::And, QList{ Term{QString(), QStringLiteral("a"), Term::Contains}, Term{QString(), QStringLiteral("b"), Term::Contains}, Term{Term::Or, QList{ Term{QString(), QStringLiteral("c"), Term::Contains}, Term{QString(), QStringLiteral("d"), Term::Contains} }} }} ; } QTEST_MAIN(AdvancedQueryParserTest) #include "advancedqueryparsertest.moc" diff --git a/src/lib/term.cpp b/src/lib/term.cpp index 71da9cad..8f99f0ae 100644 --- a/src/lib/term.cpp +++ b/src/lib/term.cpp @@ -1,459 +1,469 @@ /* * This file is part of the KDE Baloo Project * Copyright (C) 2013 Vishesh Handa * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) version 3, or any * later version accepted by the membership of KDE e.V. (or its * successor approved by the membership of KDE e.V.), which shall * act as a proxy defined in Section 6 of version 3 of the license. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . * */ #include "term.h" #include #include using namespace Baloo; class Baloo::Term::Private { public: Operation m_op; Comparator m_comp; QString m_property; QVariant m_value; bool m_isNegated; QList m_subTerms; QVariantHash m_userData; Private() { m_op = None; m_comp = Auto; m_isNegated = false; } }; Term::Term() : d(new Private) { } Term::Term(const Term& t) : d(new Private(*t.d)) { } Term::Term(const QString& property) : d(new Private) { d->m_property = property; } Term::Term(const QString& property, const QVariant& value, Term::Comparator c) : d(new Private) { d->m_property = property; d->m_value = value; if (c == Auto) { if (value.type() == QVariant::String) d->m_comp = Contains; else if (value.type() == QVariant::DateTime) d->m_comp = Contains; else d->m_comp = Equal; } else { d->m_comp = c; } } /* Term::Term(const QString& property, const QVariant& start, const QVariant& end) : d(new Private) { d->m_property = property; d->m_op = Range; // FIXME: How to save range queries? } */ Term::Term(Term::Operation op) : d(new Private) { d->m_op = op; } Term::Term(Term::Operation op, const Term& t) : d(new Private) { d->m_op = op; d->m_subTerms << t; } Term::Term(Term::Operation op, const QList& t) : d(new Private) { d->m_op = op; d->m_subTerms = t; } Term::Term(const Term& lhs, Term::Operation op, const Term& rhs) : d(new Private) { d->m_op = op; - d->m_subTerms << lhs; - d->m_subTerms << rhs; + + if (lhs.operation() == op) { + d->m_subTerms << lhs.subTerms(); + } else { + d->m_subTerms << lhs; + } + + if (rhs.operation() == op) { + d->m_subTerms << rhs.subTerms(); + } else { + d->m_subTerms << rhs; + } } Term::~Term() { delete d; } bool Term::isValid() const { // Terms with an operator but no subterms are still valid if (d->m_op != Term::None) { return true; } if (d->m_comp == Term::Auto) { return false; } return true; } void Term::setNegation(bool isNegated) { d->m_isNegated = isNegated; } bool Term::isNegated() const { return d->m_isNegated; } bool Term::negated() const { return d->m_isNegated; } void Term::addSubTerm(const Term& term) { d->m_subTerms << term; } void Term::setSubTerms(const QList& terms) { d->m_subTerms = terms; } Term Term::subTerm() const { if (d->m_subTerms.size()) return d->m_subTerms.first(); return Term(); } QList Term::subTerms() const { return d->m_subTerms; } void Term::setOperation(Term::Operation op) { d->m_op = op; } Term::Operation Term::operation() const { return d->m_op; } bool Term::empty() const { return isEmpty(); } bool Term::isEmpty() const { return d->m_property.isEmpty() && d->m_value.isNull() && d->m_subTerms.isEmpty(); } QString Term::property() const { return d->m_property; } void Term::setProperty(const QString& property) { d->m_property = property; } void Term::setValue(const QVariant& value) { d->m_value = value; } QVariant Term::value() const { return d->m_value; } Term::Comparator Term::comparator() const { return d->m_comp; } void Term::setComparator(Term::Comparator c) { d->m_comp = c; } void Term::setUserData(const QString& name, const QVariant& value) { d->m_userData.insert(name, value); } QVariant Term::userData(const QString& name) const { return d->m_userData.value(name); } QVariantMap Term::toVariantMap() const { QVariantMap map; if (d->m_op != None) { QVariantList variantList; Q_FOREACH (const Term& term, d->m_subTerms) { variantList << QVariant(term.toVariantMap()); } if (d->m_op == And) map[QStringLiteral("$and")] = variantList; else map[QStringLiteral("$or")] = variantList; return map; } QString op; switch (d->m_comp) { case Equal: map[d->m_property] = d->m_value; return map; case Contains: op = QStringLiteral("$ct"); break; case Greater: op = QStringLiteral("$gt"); break; case GreaterEqual: op = QStringLiteral("$gte"); break; case Less: op = QStringLiteral("$lt"); break; case LessEqual: op = QStringLiteral("$lte"); break; case Auto: Q_ASSERT(0); } QVariantMap m; m[op] = d->m_value; map[d->m_property] = QVariant(m); return map; } namespace { // QJson does not recognize QDate/QDateTime parameters. We try to guess // and see if they can be converted into date/datetime. QVariant tryConvert(const QVariant& var) { if (var.canConvert(QVariant::DateTime)) { QDateTime dt = var.toDateTime(); if (!dt.isValid()) return var; if (!var.toString().contains(QLatin1String("T"))) { return QVariant(var.toDate()); } return dt; } return var; } } Term Term::fromVariantMap(const QVariantMap& map) { if (map.size() != 1) return Term(); Term term; QString andOrString; if (map.contains(QStringLiteral("$and"))) { andOrString = QStringLiteral("$and"); term.setOperation(And); } else if (map.contains(QStringLiteral("$or"))) { andOrString = QStringLiteral("$or"); term.setOperation(Or); } if (andOrString.size()) { QList subTerms; QVariantList list = map[andOrString].toList(); Q_FOREACH (const QVariant& var, list) subTerms << Term::fromVariantMap(var.toMap()); term.setSubTerms(subTerms); return term; } QString prop = map.cbegin().key(); term.setProperty(prop); QVariant value = map.value(prop); if (value.type() == QVariant::Map) { QVariantMap mapVal = value.toMap(); if (mapVal.size() != 1) return term; QString op = mapVal.cbegin().key(); Term::Comparator com; if (op == QLatin1String("$ct")) com = Contains; else if (op == QLatin1String("$gt")) com = Greater; else if (op == QLatin1String("$gte")) com = GreaterEqual; else if (op == QLatin1String("$lt")) com = Less; else if (op == QLatin1String("$lte")) com = LessEqual; else return term; term.setComparator(com); term.setValue(tryConvert(mapVal.value(op))); return term; } term.setComparator(Equal); term.setValue(tryConvert(value)); return term; } bool Term::operator==(const Term& rhs) const { if (d->m_op != rhs.d->m_op || d->m_comp != rhs.d->m_comp || d->m_isNegated != rhs.d->m_isNegated || d->m_property != rhs.d->m_property || d->m_value != rhs.d->m_value) { return false; } if (d->m_subTerms.size() != rhs.d->m_subTerms.size()) return false; if (d->m_subTerms.isEmpty()) return true; Q_FOREACH (const Term& t, d->m_subTerms) { if (!rhs.d->m_subTerms.contains(t)) return false; } return true; } Term& Term::operator=(const Term& rhs) { *d = *rhs.d; return *this; } namespace { QString comparatorToString(Baloo::Term::Comparator c) { switch (c) { case Baloo::Term::Auto: return QStringLiteral("Auto"); case Baloo::Term::Equal: return QStringLiteral("="); case Baloo::Term::Contains: return QStringLiteral(":"); case Baloo::Term::Less: return QStringLiteral("<"); case Baloo::Term::LessEqual: return QStringLiteral("<="); case Baloo::Term::Greater: return QStringLiteral(">"); case Baloo::Term::GreaterEqual: return QStringLiteral(">="); } return QString(); } QString operationToString(Baloo::Term::Operation op) { switch (op) { case Baloo::Term::None: return QStringLiteral("NONE"); case Baloo::Term::And: return QStringLiteral("AND"); case Baloo::Term::Or: return QStringLiteral("OR"); } return QString(); } } QDebug operator <<(QDebug d, const Baloo::Term& t) { if (t.subTerms().isEmpty()) { d << QStringLiteral("(%1 %2 %3 (%4))").arg(t.property(), comparatorToString(t.comparator()), t.value().toString(), t.value().typeName()).toUtf8().constData(); } else { d << "(" << operationToString(t.operation()).toUtf8().constData(); for (const Term& term : t.subTerms()) { d << term; } d << ")"; } return d; } diff --git a/src/lib/term.h b/src/lib/term.h index 1c52fe3c..f45c456a 100644 --- a/src/lib/term.h +++ b/src/lib/term.h @@ -1,169 +1,163 @@ /* * This file is part of the KDE Baloo Project * Copyright (C) 2013 Vishesh Handa * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) version 3, or any * later version accepted by the membership of KDE e.V. (or its * successor approved by the membership of KDE e.V.), which shall * act as a proxy defined in Section 6 of version 3 of the license. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . * */ #ifndef BALOO_TERM_H #define BALOO_TERM_H #include #include #include namespace Baloo { class Term { public: enum Comparator { Auto, Equal, Contains, Greater, GreaterEqual, Less, LessEqual }; enum Operation { None, And, Or }; Term(); Term(const Term& t); /** * The Item must contain the property \p property */ explicit Term(const QString& property); /** * The Item must contain the property \p property with * value \value. * * The default comparator is Auto which has the following behavior * For Strings - Contains * For DateTime - Contains * For any other type - Equals */ Term(const QString& property, const QVariant& value, Comparator c = Auto); /** * This term is a combination of other terms */ Term(Operation op); Term(Operation op, const Term& t); Term(Operation op, const QList& t); Term(const Term& lhs, Operation op, const Term& rhs); ~Term(); bool isValid() const; /** * Negate this term. Negation only applies for Equal or Contains * For other Comparators you must invert it yourself */ void setNegation(bool isNegated); bool negated() const; bool isNegated() const; void addSubTerm(const Term& term); void setSubTerms(const QList& terms); /** * Returns the first subTerm in the list of subTerms */ Term subTerm() const; QList subTerms() const; void setOperation(Operation op); Operation operation() const; bool isEmpty() const; bool empty() const; /** * Return the property this term is targetting */ QString property() const; void setProperty(const QString& property); QVariant value() const; void setValue(const QVariant& value); Comparator comparator() const; void setComparator(Comparator c); void setUserData(const QString& name, const QVariant& value); QVariant userData(const QString& name) const; QVariantMap toVariantMap() const; static Term fromVariantMap(const QVariantMap& map); bool operator == (const Term& rhs) const; Term& operator=(const Term& rhs); private: class Private; Private* d; }; inline Term operator &&(const Term& lhs, const Term& rhs) { if (lhs.isEmpty()) return rhs; else if (rhs.isEmpty()) return lhs; - Term t(Term::And); - t.addSubTerm(lhs); - t.addSubTerm(rhs); - return t; + return {lhs, Term::And, rhs}; } inline Term operator ||(const Term& lhs, const Term& rhs) { if (lhs.isEmpty()) return rhs; else if (rhs.isEmpty()) return lhs; - Term t(Term::Or); - t.addSubTerm(lhs); - t.addSubTerm(rhs); - return t; + return {lhs, Term::Or, rhs}; } inline Term operator !(const Term& rhs) { Term t(rhs); t.setNegation(!rhs.isNegated()); return t; } } QDebug operator <<(QDebug d, const Baloo::Term& t); #endif