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"
|
2021-06-12 14:59:58 +02:00
|
|
|
#include "looqsgeneralexception.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
|
|
|
}
|
|
|
|
|
2019-08-17 11:06:35 +02:00
|
|
|
QString SqliteSearch::fieldToColumn(QueryField field)
|
2019-04-20 23:31:14 +02:00
|
|
|
{
|
2019-08-17 11:06:35 +02:00
|
|
|
if(field == FILE_MTIME)
|
2019-04-20 23:31:14 +02:00
|
|
|
{
|
2019-08-17 11:06:35 +02:00
|
|
|
return "file.mtime";
|
2019-04-20 23:31:14 +02:00
|
|
|
}
|
2019-08-17 11:06:35 +02:00
|
|
|
else if(field == FILE_PATH)
|
2019-04-20 23:31:14 +02:00
|
|
|
{
|
2019-08-17 11:06:35 +02:00
|
|
|
return "file.path";
|
2019-04-20 23:31:14 +02:00
|
|
|
}
|
2019-08-17 11:06:35 +02:00
|
|
|
else if(field == FILE_SIZE)
|
2019-04-25 10:27:54 +02:00
|
|
|
{
|
2019-08-17 11:06:35 +02:00
|
|
|
return "file.size";
|
2019-04-25 10:27:54 +02:00
|
|
|
}
|
2019-08-17 11:06:35 +02:00
|
|
|
else if(field == CONTENT_TEXT_PAGE)
|
2019-04-25 10:27:54 +02:00
|
|
|
{
|
|
|
|
return "content.page";
|
|
|
|
}
|
2019-08-17 11:06:35 +02:00
|
|
|
else if(field == CONTENT_TEXT)
|
2019-04-25 10:27:54 +02:00
|
|
|
{
|
2019-08-17 11:06:35 +02:00
|
|
|
return "content.text";
|
2019-04-25 10:27:54 +02:00
|
|
|
}
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
2019-08-17 11:06:35 +02:00
|
|
|
QString SqliteSearch::createSortSql(const QVector<SortCondition> sortConditions)
|
2019-04-25 10:27:54 +02:00
|
|
|
{
|
2019-08-17 11:06:35 +02:00
|
|
|
QString sortsql;
|
|
|
|
for(const SortCondition &sc : sortConditions)
|
2019-04-25 10:27:54 +02:00
|
|
|
{
|
2019-08-17 11:06:35 +02:00
|
|
|
QString order;
|
|
|
|
QString field = fieldToColumn(sc.field);
|
|
|
|
if(field == "")
|
2019-04-25 10:27:54 +02:00
|
|
|
{
|
2021-06-12 14:59:58 +02:00
|
|
|
throw LooqsGeneralException("Unknown sort field supplied");
|
2019-08-17 11:06:35 +02:00
|
|
|
}
|
|
|
|
if(sc.order == DESC)
|
|
|
|
{
|
|
|
|
order = "DESC";
|
2019-04-25 10:27:54 +02:00
|
|
|
}
|
2019-08-17 11:06:35 +02:00
|
|
|
else
|
|
|
|
{
|
|
|
|
order = "ASC";
|
|
|
|
}
|
|
|
|
sortsql += field + " " + order + ", ";
|
|
|
|
}
|
|
|
|
sortsql.chop(2);
|
|
|
|
if(!sortsql.isEmpty())
|
|
|
|
{
|
2019-05-04 20:40:43 +02:00
|
|
|
return " ORDER BY " + sortsql;
|
2019-04-25 10:27:54 +02:00
|
|
|
}
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
2022-07-28 15:10:03 +02:00
|
|
|
QString SqliteSearch::escapeFtsArgument(QString ftsArg)
|
|
|
|
{
|
|
|
|
QString result;
|
|
|
|
QRegularExpression extractor(R"#("([^"]*)"|([^\s]+))#");
|
|
|
|
QRegularExpressionMatchIterator i = extractor.globalMatch(ftsArg);
|
|
|
|
while(i.hasNext())
|
|
|
|
{
|
|
|
|
QRegularExpressionMatch m = i.next();
|
|
|
|
QString value = m.captured(1);
|
|
|
|
if(value.isEmpty())
|
|
|
|
{
|
|
|
|
value = m.captured(2);
|
2022-08-21 07:55:46 +02:00
|
|
|
if(value.endsWith('*'))
|
|
|
|
{
|
|
|
|
value = value.mid(0, value.size() - 1);
|
|
|
|
}
|
2022-10-02 19:55:10 +02:00
|
|
|
result += "\"" + value + "\"* ";
|
2022-07-28 15:10:03 +02:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2022-08-21 07:55:46 +02:00
|
|
|
result += "\"" + value + "\" ";
|
2022-07-28 15:10:03 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2019-08-17 11:06:35 +02:00
|
|
|
QPair<QString, QVector<QString>> createNonArgPair(QString key)
|
2019-04-25 10:27:54 +02:00
|
|
|
{
|
2019-08-17 11:06:35 +02:00
|
|
|
return {" " + key + " ", QVector<QString>()};
|
|
|
|
}
|
2019-04-25 10:27:54 +02:00
|
|
|
|
2019-08-17 11:06:35 +02:00
|
|
|
QPair<QString, QVector<QString>> SqliteSearch::createSql(const Token &token)
|
|
|
|
{
|
|
|
|
QPair<QString, QVector<QString>> result;
|
2019-04-20 23:31:14 +02:00
|
|
|
QString value = token.value;
|
|
|
|
value = value.replace("'", "\\'");
|
2019-08-17 11:06:35 +02:00
|
|
|
|
|
|
|
if(token.type == BOOL_AND)
|
|
|
|
{
|
|
|
|
return createNonArgPair("AND");
|
|
|
|
}
|
|
|
|
if(token.type == BOOL_OR)
|
|
|
|
{
|
|
|
|
return createNonArgPair("OR");
|
|
|
|
}
|
|
|
|
if(token.type == NEGATION)
|
2019-04-20 23:31:14 +02:00
|
|
|
{
|
2019-08-17 11:06:35 +02:00
|
|
|
return createNonArgPair("NOT");
|
2019-04-20 23:31:14 +02:00
|
|
|
}
|
2019-08-17 11:06:35 +02:00
|
|
|
if(token.type == BRACKET_OPEN)
|
2019-04-20 23:31:14 +02:00
|
|
|
{
|
2019-08-17 11:06:35 +02:00
|
|
|
return createNonArgPair("(");
|
2019-04-20 23:31:14 +02:00
|
|
|
}
|
2019-08-17 11:06:35 +02:00
|
|
|
if(token.type == BRACKET_CLOSE)
|
|
|
|
{
|
|
|
|
return createNonArgPair(")");
|
|
|
|
}
|
|
|
|
if(token.type == FILTER_PATH_STARTS)
|
2019-04-20 23:31:14 +02:00
|
|
|
{
|
2019-04-25 10:27:54 +02:00
|
|
|
return {" file.path LIKE ? || '%' ", {value}};
|
2019-04-20 23:31:14 +02:00
|
|
|
}
|
2019-08-17 11:06:35 +02:00
|
|
|
if(token.type == FILTER_PATH_ENDS)
|
2019-04-20 23:31:14 +02:00
|
|
|
{
|
2019-04-25 10:27:54 +02:00
|
|
|
return {" file.path LIKE '%' || ? ", {value}};
|
2019-04-20 23:31:14 +02:00
|
|
|
}
|
2019-08-17 11:06:35 +02:00
|
|
|
if(token.type == FILTER_PATH_CONTAINS)
|
2019-04-20 23:31:14 +02:00
|
|
|
{
|
2019-04-25 10:27:54 +02:00
|
|
|
return {" file.path LIKE '%' || ? || '%' ", {value}};
|
2019-04-20 23:31:14 +02:00
|
|
|
}
|
2019-08-17 11:06:35 +02:00
|
|
|
if(token.type == FILTER_CONTENT_PAGE)
|
2019-04-20 23:31:14 +02:00
|
|
|
{
|
2019-04-25 10:27:54 +02:00
|
|
|
return {" content.page = ?", {value}};
|
2019-04-20 23:31:14 +02:00
|
|
|
}
|
2019-08-17 11:06:35 +02:00
|
|
|
if(token.type == FILTER_CONTENT_CONTAINS)
|
2019-04-20 23:31:14 +02:00
|
|
|
{
|
2022-10-18 16:06:08 +02:00
|
|
|
return {" fts MATCH ? ", {escapeFtsArgument(value)}};
|
2019-04-20 23:31:14 +02:00
|
|
|
}
|
2021-06-12 14:59:58 +02:00
|
|
|
throw LooqsGeneralException("Unknown token passed (should not happen)");
|
2019-04-20 23:31:14 +02:00
|
|
|
}
|
|
|
|
|
2021-06-12 14:59:58 +02:00
|
|
|
QSqlQuery SqliteSearch::makeSqlQuery(const LooqsQuery &query)
|
2019-04-20 23:31:14 +02:00
|
|
|
{
|
2019-04-25 10:27:54 +02:00
|
|
|
QString whereSql;
|
2021-03-07 22:23:12 +01:00
|
|
|
QString joinSql;
|
2019-04-25 10:27:54 +02:00
|
|
|
QVector<QString> bindValues;
|
2021-06-12 22:50:27 +02:00
|
|
|
bool isContentSearch = (query.getTokensMask() & FILTER_CONTENT) == FILTER_CONTENT;
|
2019-08-17 11:06:35 +02:00
|
|
|
if(query.getTokens().isEmpty())
|
2019-04-20 23:31:14 +02:00
|
|
|
{
|
2021-06-12 14:59:58 +02:00
|
|
|
throw LooqsGeneralException("Nothing to search for supplied");
|
2019-04-20 23:31:14 +02:00
|
|
|
}
|
|
|
|
|
2022-04-17 21:29:25 +02:00
|
|
|
bool ftsAlreadyJoined = false;
|
2022-04-17 21:15:58 +02:00
|
|
|
auto tokens = query.getTokens();
|
|
|
|
for(const Token &token : tokens)
|
2019-05-04 20:40:43 +02:00
|
|
|
{
|
2022-10-18 16:06:08 +02:00
|
|
|
auto sql = createSql(token);
|
|
|
|
whereSql += sql.first;
|
|
|
|
bindValues.append(sql.second);
|
2019-05-04 20:40:43 +02:00
|
|
|
}
|
2019-08-17 11:06:35 +02:00
|
|
|
|
|
|
|
QString prepSql;
|
|
|
|
QString sortSql = createSortSql(query.getSortConditions());
|
2022-10-18 16:06:08 +02:00
|
|
|
int bindIterations = 1;
|
2019-04-25 10:27:54 +02:00
|
|
|
if(isContentSearch)
|
2019-04-22 21:07:41 +02:00
|
|
|
{
|
2021-03-07 22:23:12 +01:00
|
|
|
if(sortSql.isEmpty())
|
|
|
|
{
|
2022-04-17 21:15:58 +02:00
|
|
|
if(std::find_if(tokens.begin(), tokens.end(),
|
|
|
|
[](const Token &t) -> bool { return t.type == FILTER_CONTENT_CONTAINS; }) != tokens.end())
|
|
|
|
{
|
2022-10-18 16:06:08 +02:00
|
|
|
sortSql = "ORDER BY prio, rank";
|
2022-04-17 21:15:58 +02:00
|
|
|
}
|
2021-03-07 22:23:12 +01:00
|
|
|
}
|
2022-10-18 16:06:08 +02:00
|
|
|
QString whereSqlTrigram = whereSql;
|
|
|
|
whereSqlTrigram.replace("fts MATCH", "fts_trigram MATCH"); // A bit dirty...
|
|
|
|
prepSql =
|
|
|
|
"SELECT DISTINCT path, page, mtime, size, filetype FROM ("
|
|
|
|
"SELECT file.path AS path, content.page AS page, file.mtime AS mtime, file.size AS size, "
|
|
|
|
"file.filetype AS filetype, 0 AS prio, fts.rank AS rank FROM file INNER JOIN content ON file.id = "
|
|
|
|
"content.fileid "
|
|
|
|
"INNER JOIN fts ON content.ftsid = fts.ROWID WHERE 1=1 AND " +
|
|
|
|
whereSql +
|
|
|
|
"UNION ALL SELECT file.path AS path, content.page AS page, file.mtime AS mtime, file.size AS size, "
|
|
|
|
"file.filetype AS filetype, 1 as prio, fts_trigram.rank AS rank FROM file INNER JOIN content ON file.id = "
|
|
|
|
"content.fileid " +
|
|
|
|
"INNER JOIN fts_trigram ON content.fts_trigramid = fts_trigram.ROWID WHERE 1=1 AND " + whereSqlTrigram +
|
|
|
|
" ) " + sortSql;
|
|
|
|
++bindIterations;
|
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";
|
|
|
|
}
|
2022-08-24 00:00:09 +02:00
|
|
|
prepSql = "SELECT file.path AS path, '0' as page, 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
|
|
|
|
2022-06-05 14:04:17 +02:00
|
|
|
if(query.getLimit() > 0)
|
|
|
|
{
|
|
|
|
prepSql += " LIMIT " + QString::number(query.getLimit());
|
|
|
|
}
|
|
|
|
|
2019-04-22 21:07:41 +02:00
|
|
|
QSqlQuery dbquery(*db);
|
2019-04-25 10:27:54 +02:00
|
|
|
dbquery.prepare(prepSql);
|
2022-10-18 16:06:08 +02:00
|
|
|
for(int i = 0; i < bindIterations; i++)
|
2019-04-25 10:27:54 +02:00
|
|
|
{
|
2022-10-18 16:06:08 +02:00
|
|
|
for(const QString &value : bindValues)
|
2019-04-25 10:27:54 +02:00
|
|
|
{
|
2022-10-18 16:06:08 +02:00
|
|
|
if(value != "")
|
|
|
|
{
|
|
|
|
dbquery.addBindValue(value);
|
|
|
|
}
|
2019-04-25 10:27:54 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return dbquery;
|
|
|
|
}
|
|
|
|
|
2021-06-12 14:59:58 +02:00
|
|
|
QVector<SearchResult> SqliteSearch::search(const LooqsQuery &query)
|
2019-04-25 10:27:54 +02:00
|
|
|
{
|
|
|
|
QVector<SearchResult> results;
|
2019-08-17 11:06:35 +02:00
|
|
|
QSqlQuery dbQuery = makeSqlQuery(query);
|
2019-04-25 10:27:54 +02:00
|
|
|
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();
|
2021-06-12 14:59:58 +02:00
|
|
|
throw LooqsGeneralException("SQL Error: " + dbQuery.lastError().text());
|
2019-04-22 21:07:41 +02:00
|
|
|
}
|
|
|
|
|
2022-06-13 22:44:24 +02:00
|
|
|
bool contentSearch = query.hasContentSearch();
|
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();
|
2022-08-24 00:00:09 +02:00
|
|
|
result.page = dbQuery.value("page").toUInt();
|
2022-06-13 22:44:24 +02:00
|
|
|
result.wasContentSearch = contentSearch;
|
2019-04-22 21:07:41 +02:00
|
|
|
results.append(result);
|
|
|
|
}
|
|
|
|
return results;
|
2019-04-20 23:31:14 +02:00
|
|
|
}
|