2019-04-20 23:31:14 +02:00
|
|
|
#include <QStack>
|
|
|
|
#include <QRegularExpression>
|
2019-04-22 21:07:41 +02:00
|
|
|
#include <QSqlQuery>
|
|
|
|
#include <QSqlError>
|
2019-04-25 10:27:54 +02:00
|
|
|
#include <QStringList>
|
2019-04-22 21:07:41 +02:00
|
|
|
#include <QDebug>
|
2019-04-20 23:31:14 +02:00
|
|
|
#include "sqlitesearch.h"
|
2019-04-22 21:07:41 +02:00
|
|
|
#include "qssgeneralexception.h"
|
2019-04-20 23:31:14 +02:00
|
|
|
|
2019-04-22 21:07:41 +02:00
|
|
|
SqliteSearch::SqliteSearch(QSqlDatabase &db)
|
2019-04-20 23:31:14 +02:00
|
|
|
{
|
2019-04-22 21:07:41 +02:00
|
|
|
this->db = &db;
|
2019-04-20 23:31:14 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
QVector<SqliteSearch::Token> SqliteSearch::tokenize(QString expression)
|
|
|
|
{
|
|
|
|
if(!checkParanthesis(expression))
|
|
|
|
{
|
2019-04-22 21:07:41 +02:00
|
|
|
throw QSSGeneralException("Invalid paranthesis");
|
2019-04-20 23:31:14 +02:00
|
|
|
}
|
|
|
|
// TODO: merge lonewords
|
|
|
|
QVector<SqliteSearch::Token> result;
|
2019-05-04 20:40:43 +02:00
|
|
|
QRegularExpression rx("((?<filtername>(\\.|\\w)+):(?<args>\\((?<innerargs>[^\\)]+)\\)|([\\w,])+)|(?<boolean>AND|OR|"
|
|
|
|
"!)|(?<bracket>\\(|\\))|(?<loneword>\\w+))");
|
2019-04-20 23:31:14 +02:00
|
|
|
QRegularExpressionMatchIterator i = rx.globalMatch(expression);
|
2019-05-04 20:40:43 +02:00
|
|
|
auto isSort = [](QString &key) { return key == "sort"; };
|
|
|
|
auto isBool = [](QString &key) { return key == "AND" || key == "OR" || key == "!"; };
|
2019-04-20 23:31:14 +02:00
|
|
|
while(i.hasNext())
|
|
|
|
{
|
|
|
|
QRegularExpressionMatch m = i.next();
|
|
|
|
QString boolean = m.captured("boolean");
|
|
|
|
QString filtername = m.captured("filtername");
|
|
|
|
QString bracket = m.captured("bracket");
|
|
|
|
QString loneword = m.captured("loneword");
|
|
|
|
|
|
|
|
if(boolean != "")
|
|
|
|
{
|
|
|
|
result.append(Token(boolean));
|
|
|
|
}
|
|
|
|
|
2019-05-04 20:40:43 +02:00
|
|
|
if(!result.empty())
|
2019-04-20 23:31:14 +02:00
|
|
|
{
|
2019-05-04 20:40:43 +02:00
|
|
|
QString &lastKey = result.last().key;
|
|
|
|
if(!isBool(lastKey) && !isSort(lastKey) && !isSort(filtername))
|
2019-04-20 23:31:14 +02:00
|
|
|
{
|
2019-05-04 20:40:43 +02:00
|
|
|
result.append(Token("AND"));
|
2019-04-20 23:31:14 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-05-04 20:40:43 +02:00
|
|
|
if(bracket != "")
|
2019-04-20 23:31:14 +02:00
|
|
|
{
|
2019-05-04 20:40:43 +02:00
|
|
|
if(bracket == "(")
|
2019-04-20 23:31:14 +02:00
|
|
|
{
|
|
|
|
result.append(Token("AND"));
|
|
|
|
}
|
2019-05-04 20:40:43 +02:00
|
|
|
result.append(Token(bracket));
|
|
|
|
}
|
|
|
|
|
|
|
|
if(loneword != "")
|
|
|
|
{
|
2019-04-20 23:31:14 +02:00
|
|
|
result.append(Token("path.contains", loneword));
|
|
|
|
}
|
|
|
|
|
|
|
|
if(filtername != "")
|
|
|
|
{
|
|
|
|
QString value = m.captured("innerargs");
|
|
|
|
if(value == "")
|
|
|
|
{
|
|
|
|
value = m.captured("args");
|
|
|
|
}
|
|
|
|
result.append(Token(filtername, value));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2019-04-25 10:27:54 +02:00
|
|
|
QString SqliteSearch::fieldToColumn(QString field)
|
2019-04-20 23:31:14 +02:00
|
|
|
{
|
2019-04-25 10:27:54 +02:00
|
|
|
if(field == "mtime" || field == "file.mtime")
|
|
|
|
{
|
|
|
|
return "file.mtime";
|
|
|
|
}
|
|
|
|
else if(field == "page" || field == "content.page")
|
|
|
|
{
|
|
|
|
return "content.page";
|
|
|
|
}
|
|
|
|
else if(field == "path" || field == "file.path")
|
|
|
|
{
|
|
|
|
return "file.path";
|
|
|
|
}
|
|
|
|
else if(field == "size" || field == "file.size")
|
|
|
|
{
|
|
|
|
return "file.size";
|
|
|
|
}
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
|
|
|
QString SqliteSearch::createSortSql(const SqliteSearch::Token &token)
|
|
|
|
{
|
|
|
|
// sort:(mtime desc, page asc)
|
|
|
|
if(token.key == "sort")
|
|
|
|
{
|
2019-04-27 21:23:06 +02:00
|
|
|
QString sortsql;
|
2019-04-25 10:27:54 +02:00
|
|
|
QStringList splitted_inner = token.value.split(",");
|
|
|
|
for(int i = 0; i < splitted_inner.length(); i++)
|
|
|
|
{
|
|
|
|
QStringList splitted = splitted_inner[i].split(" ");
|
2019-05-04 20:40:43 +02:00
|
|
|
if(splitted.length() < 1 || splitted.length() > 2)
|
|
|
|
{
|
|
|
|
throw QSSGeneralException("sort specifier must have format [field] (asc|desc)");
|
|
|
|
}
|
|
|
|
|
|
|
|
QString field = splitted[0];
|
|
|
|
field = fieldToColumn(field);
|
|
|
|
if(field == "")
|
|
|
|
{
|
|
|
|
throw QSSGeneralException("Unknown sort field supplied");
|
|
|
|
}
|
|
|
|
|
|
|
|
QString order;
|
2019-04-25 10:27:54 +02:00
|
|
|
if(splitted.length() == 2)
|
|
|
|
{
|
2019-05-04 20:40:43 +02:00
|
|
|
order = splitted[1];
|
|
|
|
if(order.compare("asc", Qt::CaseInsensitive) == 0)
|
2019-04-25 10:27:54 +02:00
|
|
|
{
|
|
|
|
order = "ASC";
|
|
|
|
}
|
2019-05-04 20:40:43 +02:00
|
|
|
else if(order.compare("desc", Qt::CaseInsensitive) == 0)
|
2019-04-25 10:27:54 +02:00
|
|
|
{
|
|
|
|
order = "DESC";
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
throw QSSGeneralException("Unknown order specifier: " + order);
|
|
|
|
}
|
|
|
|
}
|
2019-05-04 20:40:43 +02:00
|
|
|
else
|
2019-04-25 10:27:54 +02:00
|
|
|
{
|
2019-05-04 20:40:43 +02:00
|
|
|
order = "ASC";
|
2019-04-25 10:27:54 +02:00
|
|
|
}
|
2019-05-04 20:40:43 +02:00
|
|
|
|
|
|
|
sortsql += field + " " + order;
|
|
|
|
if(splitted_inner.length() - i > 1)
|
2019-04-25 10:27:54 +02:00
|
|
|
{
|
2019-05-04 20:40:43 +02:00
|
|
|
sortsql += ", ";
|
2019-04-25 10:27:54 +02:00
|
|
|
}
|
|
|
|
}
|
2019-05-04 20:40:43 +02:00
|
|
|
return " ORDER BY " + sortsql;
|
2019-04-25 10:27:54 +02:00
|
|
|
}
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
|
|
|
QPair<QString, QVector<QString>> SqliteSearch::createSql(const SqliteSearch::Token &token)
|
|
|
|
{
|
|
|
|
QPair<QString, QVector<QString>> result;
|
|
|
|
|
2019-04-20 23:31:14 +02:00
|
|
|
QString key = token.key;
|
|
|
|
QString value = token.value;
|
|
|
|
value = value.replace("'", "\\'");
|
|
|
|
if(key == "AND" || key == "OR" || key == "(" || key == ")")
|
|
|
|
{
|
2019-04-25 10:27:54 +02:00
|
|
|
return {" " + key + " ", QVector<QString>()};
|
2019-04-20 23:31:14 +02:00
|
|
|
}
|
|
|
|
if(key == "!")
|
|
|
|
{
|
2019-04-25 10:27:54 +02:00
|
|
|
return {" NOT ", QVector<QString>()};
|
2019-04-20 23:31:14 +02:00
|
|
|
}
|
|
|
|
if(key == "path.starts")
|
|
|
|
{
|
2019-04-25 10:27:54 +02:00
|
|
|
return {" file.path LIKE ? || '%' ", {value}};
|
2019-04-20 23:31:14 +02:00
|
|
|
}
|
|
|
|
if(key == "path.ends")
|
|
|
|
{
|
2019-04-25 10:27:54 +02:00
|
|
|
return {" file.path LIKE '%' || ? ", {value}};
|
2019-04-20 23:31:14 +02:00
|
|
|
}
|
|
|
|
if(key == "path.contains" || key == "inpath")
|
|
|
|
{
|
2019-04-25 10:27:54 +02:00
|
|
|
return {" file.path LIKE '%' || ? || '%' ", {value}};
|
2019-04-20 23:31:14 +02:00
|
|
|
}
|
|
|
|
if(key == "page")
|
|
|
|
{
|
2019-04-25 10:27:54 +02:00
|
|
|
return {" content.page = ?", {value}};
|
2019-04-20 23:31:14 +02:00
|
|
|
}
|
|
|
|
if(key == "contains" || key == "c")
|
|
|
|
{
|
2019-04-25 10:27:54 +02:00
|
|
|
return {" content.id IN (SELECT content_fts.ROWID FROM content_fts WHERE content_fts.content MATCH ?) ",
|
|
|
|
{value}};
|
2019-04-20 23:31:14 +02:00
|
|
|
}
|
2019-04-22 23:16:29 +02:00
|
|
|
throw QSSGeneralException("Unknown token: " + key);
|
2019-04-20 23:31:14 +02:00
|
|
|
}
|
|
|
|
|
2019-04-25 10:27:54 +02:00
|
|
|
QSqlQuery SqliteSearch::makeSqlQuery(const QVector<SqliteSearch::Token> &tokens)
|
2019-04-20 23:31:14 +02:00
|
|
|
{
|
2019-04-25 10:27:54 +02:00
|
|
|
QString whereSql;
|
|
|
|
QString limitSql;
|
2019-04-26 21:41:20 +02:00
|
|
|
QString sortSql;
|
2019-04-25 10:27:54 +02:00
|
|
|
QVector<QString> bindValues;
|
|
|
|
bool isContentSearch = false;
|
2019-04-20 23:31:14 +02:00
|
|
|
for(const Token &c : tokens)
|
|
|
|
{
|
2019-04-25 10:27:54 +02:00
|
|
|
if(c.key == "sort")
|
|
|
|
{
|
|
|
|
if(sortSql != "")
|
|
|
|
{
|
|
|
|
throw QSSGeneralException("Invalid input: Two seperate sort statements are invalid");
|
|
|
|
}
|
|
|
|
sortSql = createSortSql(c);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
if(c.key == "c" || c.key == "contains")
|
|
|
|
{
|
|
|
|
isContentSearch = true;
|
|
|
|
}
|
|
|
|
auto sql = createSql(c);
|
|
|
|
whereSql += sql.first;
|
|
|
|
bindValues.append(sql.second);
|
|
|
|
}
|
2019-04-20 23:31:14 +02:00
|
|
|
}
|
|
|
|
|
2019-04-25 10:27:54 +02:00
|
|
|
QString prepSql;
|
2019-05-04 20:40:43 +02:00
|
|
|
if(whereSql.isEmpty())
|
|
|
|
{
|
|
|
|
throw QSSGeneralException("Nothing to search for supplied");
|
|
|
|
}
|
2019-04-25 10:27:54 +02:00
|
|
|
if(isContentSearch)
|
2019-04-22 21:07:41 +02:00
|
|
|
{
|
2019-04-26 21:41:20 +02:00
|
|
|
if(sortSql.isEmpty())
|
|
|
|
{
|
|
|
|
sortSql = "ORDER BY file.mtime DESC, content.page ASC";
|
|
|
|
}
|
2019-04-26 15:31:42 +02:00
|
|
|
prepSql =
|
|
|
|
"SELECT file.path AS path, group_concat(content.page) AS pages, file.mtime AS mtime, file.size AS size, "
|
|
|
|
"file.filetype AS filetype FROM file INNER JOIN content ON file.id = content.fileid WHERE 1=1 AND " +
|
2019-04-27 21:23:06 +02:00
|
|
|
whereSql + " GROUP BY file.path " + sortSql;
|
2019-04-22 21:07:41 +02:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2019-04-26 21:41:20 +02:00
|
|
|
if(sortSql.isEmpty())
|
|
|
|
{
|
|
|
|
sortSql = "ORDER BY file.mtime DESC";
|
|
|
|
}
|
2019-05-04 20:40:43 +02:00
|
|
|
if(sortSql.contains("content."))
|
|
|
|
{
|
|
|
|
throw QSSGeneralException("Cannot sort for content fields when not doing a content search");
|
|
|
|
}
|
|
|
|
|
2019-04-26 15:31:42 +02:00
|
|
|
prepSql = "SELECT file.path AS path, '0' as pages, file.mtime AS mtime, file.size AS size, file.filetype AS "
|
2019-04-25 10:27:54 +02:00
|
|
|
"filetype FROM file WHERE 1=1 AND " +
|
|
|
|
whereSql + " " + sortSql;
|
2019-04-22 21:07:41 +02:00
|
|
|
}
|
2019-04-25 10:27:54 +02:00
|
|
|
|
2019-04-22 21:07:41 +02:00
|
|
|
QSqlQuery dbquery(*db);
|
2019-04-25 10:27:54 +02:00
|
|
|
dbquery.prepare(prepSql);
|
|
|
|
|
|
|
|
for(const QString &value : bindValues)
|
|
|
|
{
|
|
|
|
if(value != "")
|
|
|
|
{
|
|
|
|
dbquery.addBindValue(value);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return dbquery;
|
|
|
|
}
|
|
|
|
|
|
|
|
QVector<SearchResult> SqliteSearch::search(const QString &query)
|
|
|
|
{
|
|
|
|
QVector<SearchResult> results;
|
|
|
|
QSqlQuery dbQuery = makeSqlQuery(tokenize(query));
|
|
|
|
bool success = dbQuery.exec();
|
2019-04-22 21:07:41 +02:00
|
|
|
if(!success)
|
|
|
|
{
|
2019-04-25 10:27:54 +02:00
|
|
|
|
|
|
|
qDebug() << dbQuery.lastError();
|
2019-04-27 21:23:06 +02:00
|
|
|
qDebug() << dbQuery.executedQuery();
|
2019-04-25 10:27:54 +02:00
|
|
|
throw QSSGeneralException("SQL Error: " + dbQuery.lastError().text());
|
2019-04-22 21:07:41 +02:00
|
|
|
}
|
|
|
|
|
2019-04-25 10:27:54 +02:00
|
|
|
while(dbQuery.next())
|
2019-04-22 21:07:41 +02:00
|
|
|
{
|
|
|
|
SearchResult result;
|
2019-04-25 10:27:54 +02:00
|
|
|
result.fileData.absPath = dbQuery.value("path").toString();
|
|
|
|
result.fileData.mtime = dbQuery.value("mtime").toUInt();
|
|
|
|
result.fileData.size = dbQuery.value("size").toUInt();
|
|
|
|
result.fileData.filetype = dbQuery.value("filetype").toChar();
|
2019-04-26 15:31:42 +02:00
|
|
|
QString pages = dbQuery.value("pages").toString();
|
|
|
|
QStringList pagesList = pages.split(",");
|
|
|
|
for(QString &page : pagesList)
|
|
|
|
{
|
|
|
|
if(page != "")
|
|
|
|
{
|
|
|
|
result.pages.append(page.toUInt());
|
|
|
|
}
|
|
|
|
}
|
2019-04-22 21:07:41 +02:00
|
|
|
results.append(result);
|
|
|
|
}
|
|
|
|
return results;
|
2019-04-20 23:31:14 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
bool SqliteSearch::checkParanthesis(QString expression)
|
|
|
|
{
|
|
|
|
QStack<QChar> open;
|
|
|
|
QStack<QChar> close;
|
|
|
|
|
|
|
|
for(QChar &c : expression)
|
|
|
|
{
|
|
|
|
if(c == '(')
|
|
|
|
{
|
|
|
|
open.push(c);
|
|
|
|
}
|
|
|
|
if(c == ')')
|
|
|
|
{
|
|
|
|
close.push(c);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if(open.size() != close.size())
|
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
while(!open.empty() && !close.empty())
|
|
|
|
{
|
|
|
|
QChar o = open.pop();
|
|
|
|
QChar c = close.pop();
|
|
|
|
if(o != '(' && c != ')')
|
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|