Begin sort: statement implementation, use bindvalues in all search filters, gui: save current scale
This commit is contained in:
		| @@ -13,6 +13,7 @@ | ||||
| #include "mainwindow.h" | ||||
| #include "ui_mainwindow.h" | ||||
| #include "clicklabel.h" | ||||
| #include "../shared/sqlitesearch.h" | ||||
| MainWindow::MainWindow(QWidget *parent) : | ||||
|     QMainWindow(parent), | ||||
|     ui(new Ui::MainWindow) | ||||
| @@ -31,6 +32,8 @@ MainWindow::MainWindow(QWidget *parent) : | ||||
|     ui->statusBar->addWidget(ui->lblSearchResults); | ||||
|     ui->statusBar->addWidget(ui->pdfProcessBar); | ||||
|     ui->pdfProcessBar->hide(); | ||||
|     QSettings settings; | ||||
|     ui->comboScale->setCurrentText(settings.value("currentScale").toString()); | ||||
| } | ||||
|  | ||||
| void MainWindow::connectSignals() | ||||
| @@ -51,6 +54,8 @@ void MainWindow::connectSignals() | ||||
|  | ||||
| void MainWindow::comboScaleChanged(QString text) | ||||
| { | ||||
|     QSettings scaleSetting; | ||||
|     scaleSetting.setValue("currentScale", ui->comboScale->currentText()); | ||||
|     makePdfPreview(); | ||||
| } | ||||
| bool MainWindow::pdfTabActive() | ||||
| @@ -126,7 +131,7 @@ void MainWindow::pdfPreviewReceived(PdfPreview preview) | ||||
| void MainWindow::lineEditReturnPressed() | ||||
| { | ||||
|     QString q = ui->txtSearch->text(); | ||||
|     if(!searchWorker->checkParanthesis(q)) | ||||
|     if(!SqliteSearch::checkParanthesis(q)) | ||||
|     { | ||||
|         ui->lblSearchResults->setText("Invalid paranthesis"); | ||||
|         return; | ||||
|   | ||||
| @@ -19,157 +19,9 @@ SearchWorker::SearchWorker(const QString &dbpath) | ||||
|     } | ||||
| } | ||||
|  | ||||
| QVector<SearchWorker::Command> SearchWorker::tokenize(QString expression) | ||||
| { | ||||
|     if(!checkParanthesis(expression)) | ||||
|     { | ||||
|         throw std::invalid_argument("Invalid paranthesis"); | ||||
|     } | ||||
|     //TODO: merge lonewords | ||||
|     QVector<Command> result; | ||||
|     QRegularExpression rx("((?<filtername>(\\.|\\w)+):(?<args>\\((?<innerargs>[^\\)]+)\\)|(\\w)+)|(?<boolean>AND|OR|!)|(?<bracket>\\(|\\))|(?<loneword>\\w+))"); | ||||
|     QRegularExpressionMatchIterator i = rx.globalMatch(expression); | ||||
|     bool wasbool = true; | ||||
|     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 != "") | ||||
|         { | ||||
|            /* if(wasbool) | ||||
|             { | ||||
|                 throw new std::runtime_error("Bool after Bool is invalid"); | ||||
|             }*/ | ||||
|             wasbool = true; | ||||
|             result.append(Command(boolean)); | ||||
|         } | ||||
|  | ||||
|         if(bracket != "") | ||||
|         { | ||||
|            if(!wasbool) | ||||
|            { | ||||
|                if(bracket == "(") | ||||
|                { | ||||
|                    result.append(Command("AND")); | ||||
|                } | ||||
|            } | ||||
|            result.append(Command(bracket)); | ||||
|         } | ||||
|  | ||||
|  | ||||
|         if(loneword != "") | ||||
|         { | ||||
|             if(!wasbool) | ||||
|             { | ||||
|                 result.append(Command("AND")); | ||||
|             } | ||||
|             wasbool = false; | ||||
|             result.append(Command("path.contains", loneword)); | ||||
|         } | ||||
|         if(filtername != "") | ||||
|         { | ||||
|             if(!wasbool) | ||||
|             { | ||||
|                 result.append(Command("AND")); | ||||
|             } | ||||
|             wasbool = false; | ||||
|             QString value = m.captured("innerargs"); | ||||
|             if(value == "") | ||||
|                 value = m.captured("args"); | ||||
|             result.append(Command(filtername, value)); | ||||
|  | ||||
|         } | ||||
|  | ||||
|     } | ||||
|     return result; | ||||
| } | ||||
|  | ||||
| QString SearchWorker::createSql(const SearchWorker::Command &cmd) | ||||
| { | ||||
|     QString key = cmd.key; | ||||
|     QString value = cmd.value; | ||||
|     value = value.replace("'", "\\'"); | ||||
|     if(key == "AND" || key == "OR" || key=="(" || key==")") | ||||
|     { | ||||
|         return " " + key + " "; | ||||
|     } | ||||
|     if(key == "!") | ||||
|     { | ||||
|         return " NOT "; | ||||
|     } | ||||
|     if(key == "path.starts") | ||||
|     { | ||||
|         return " file.path LIKE '" + value + "%' "; | ||||
|     } | ||||
|     if(key == "path.ends") | ||||
|     { | ||||
|         return " file.path LIKE '%" + value + "' "; | ||||
|     } | ||||
|     if(key == "path.contains" || key == "inpath") | ||||
|     { | ||||
|         return " file.path LIKE '%" + value + "%' "; | ||||
|     } | ||||
|     if(key == "page") | ||||
|     { | ||||
|         return " content.page = " + value; | ||||
|     } | ||||
|     if(key == "contains" || key == "c") | ||||
|     { | ||||
|         return " content.id IN (SELECT content_fts.ROWID FROM content_fts WHERE content_fts.content MATCH '" + value + "' )"; | ||||
|     } | ||||
|  | ||||
|     throw std::invalid_argument("Unknown filter: " + key.toStdString()); | ||||
| } | ||||
|  | ||||
| QString SearchWorker::makeSql(const QVector<SearchWorker::Command> &tokens) | ||||
| { | ||||
|     QString result; | ||||
|     for(const Command &c : tokens) | ||||
|     { | ||||
|         result += createSql(c); | ||||
|     } | ||||
|     return result; | ||||
| } | ||||
|  | ||||
| bool SearchWorker::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; | ||||
| } | ||||
|  | ||||
| void SearchWorker::search(const QString &query) | ||||
| { | ||||
|     SqliteSearch searcher(db); | ||||
|     emit searchResultsReady(searcher.search(query)); | ||||
|  | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -2,6 +2,7 @@ | ||||
| #include <QRegularExpression> | ||||
| #include <QSqlQuery> | ||||
| #include <QSqlError> | ||||
| #include <QStringList> | ||||
| #include <QDebug> | ||||
| #include "sqlitesearch.h" | ||||
| #include "qssgeneralexception.h" | ||||
| @@ -79,85 +80,195 @@ QVector<SqliteSearch::Token> SqliteSearch::tokenize(QString expression) | ||||
| } | ||||
|  | ||||
|  | ||||
|  | ||||
| QString SqliteSearch::createSql(const SqliteSearch::Token &token) | ||||
| QString SqliteSearch::fieldToColumn(QString field) | ||||
| { | ||||
|     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") | ||||
|     { | ||||
|         QString sortsql = " ORDER BY "; | ||||
|         QStringList splitted_inner = token.value.split(","); | ||||
|         for(int i = 0; i < splitted_inner.length(); i++) | ||||
|         { | ||||
|             QStringList splitted = splitted_inner[i].split(" "); | ||||
|             if(splitted.length() == 2) | ||||
|             { | ||||
|                 QString field = splitted[0]; | ||||
|                 QString order = splitted[1]; | ||||
|                 if(order.compare("asc", Qt::CaseInsensitive)) | ||||
|                 { | ||||
|                     order = "ASC"; | ||||
|                 } | ||||
|                 else if(order.compare("desc", Qt::CaseInsensitive)) | ||||
|                 { | ||||
|                     order = "DESC"; | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     throw QSSGeneralException("Unknown order specifier: " + order); | ||||
|                 } | ||||
|  | ||||
|                 field = fieldToColumn(field); | ||||
|                 if(field == "") | ||||
|                 { | ||||
|                     throw QSSGeneralException("Unknown field:" + field); | ||||
|                 } | ||||
|  | ||||
|                 sortsql += field + " " + order; | ||||
|                 if(splitted_inner.length() - i > 1) | ||||
|                 { | ||||
|                     sortsql += ", "; | ||||
|                 } | ||||
|             } | ||||
|             else if(splitted.length() == 1) | ||||
|             { | ||||
|                 sortsql +=  splitted[0] + " ASC "; | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 throw QSSGeneralException("sort specifier must have format [field] (asc|desc)"); | ||||
|             } | ||||
|  | ||||
|  | ||||
|         } | ||||
|         return sortsql; | ||||
|     } | ||||
|     return ""; | ||||
| } | ||||
|  | ||||
| QPair<QString, QVector<QString>> SqliteSearch::createSql(const SqliteSearch::Token &token) | ||||
| { | ||||
|     QPair<QString, QVector<QString>> result; | ||||
|  | ||||
|     QString key = token.key; | ||||
|     QString value = token.value; | ||||
|     value = value.replace("'", "\\'"); | ||||
|     if(key == "AND" || key == "OR" || key == "(" || key == ")") | ||||
|     { | ||||
|         return " " + key + " "; | ||||
|         return { " " + key + " ", QVector<QString>() }; | ||||
|     } | ||||
|     if(key == "!") | ||||
|     { | ||||
|         return " NOT "; | ||||
|         return { " NOT ", QVector<QString>() }; | ||||
|     } | ||||
|     if(key == "path.starts") | ||||
|     { | ||||
|         return " file.path LIKE '" + value + "%' "; | ||||
|         return { " file.path LIKE ? || '%' ", { value } }; | ||||
|     } | ||||
|     if(key == "path.ends") | ||||
|     { | ||||
|         return " file.path LIKE '%" + value + "' "; | ||||
|         return { " file.path LIKE '%' || ? ", { value } }; | ||||
|     } | ||||
|     if(key == "path.contains" || key == "inpath") | ||||
|     { | ||||
|         return " file.path LIKE '%" + value + "%' "; | ||||
|         return { " file.path LIKE '%' || ? || '%' ", { value } } ; | ||||
|     } | ||||
|     if(key == "page") | ||||
|     { | ||||
|         return " content.page = " + value; | ||||
|         return { " content.page = ?", { value } }; | ||||
|     } | ||||
|     if(key == "contains" || key == "c") | ||||
|     { | ||||
|         return " content.id IN (SELECT content_fts.ROWID FROM content_fts WHERE content_fts.content MATCH '" + value + "' )"; | ||||
|         return { " content.id IN (SELECT content_fts.ROWID FROM content_fts WHERE content_fts.content MATCH ?) " , { value } }; | ||||
|     } | ||||
|     throw QSSGeneralException("Unknown token: " + key); | ||||
| } | ||||
|  | ||||
| QString SqliteSearch::makeSql(const QVector<SqliteSearch::Token> &tokens) | ||||
| QSqlQuery SqliteSearch::makeSqlQuery(const QVector<SqliteSearch::Token> &tokens) | ||||
| { | ||||
|     QString result; | ||||
|     QString whereSql; | ||||
|     QString sortSql; | ||||
|     QString limitSql; | ||||
|     QVector<QString> bindValues; | ||||
|     bool isContentSearch = false; | ||||
|     for(const Token &c : tokens) | ||||
|     { | ||||
|         result += createSql(c); | ||||
|         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); | ||||
|         } | ||||
|     } | ||||
|     return result; | ||||
|  | ||||
|     QString prepSql; | ||||
|     if(isContentSearch) | ||||
|     { | ||||
|         prepSql = "SELECT file.path AS path, content.page AS page, 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 " + whereSql + " " + sortSql; | ||||
|     } | ||||
|     else | ||||
|     { | ||||
|         prepSql = "SELECT file.path AS path, 0 as page,  file.mtime AS mtime, file.size AS size, file.filetype AS filetype FROM file WHERE  1=1 AND " + whereSql + " " + sortSql; | ||||
|     } | ||||
|  | ||||
|     QSqlQuery dbquery(*db); | ||||
|     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; | ||||
|     QString whereSql = makeSql(tokenize(query)); | ||||
|     QString prep; | ||||
|     //TODO: hack, as we don't wanna look into content and get redundant results, when we don't even care about content | ||||
|     if(whereSql.contains("content.")) | ||||
|     { | ||||
|         prep = "SELECT file.path AS path, content.page AS page, 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 " + whereSql + " ORDER By file.mtime DESC, content.page ASC"; | ||||
|     } | ||||
|     else | ||||
|     { | ||||
|         prep = "SELECT file.path AS path, 0 as page,  file.mtime AS mtime, file.size AS size, file.filetype AS filetype FROM file WHERE " + whereSql + " ORDER by file.mtime DESC"; | ||||
|     } | ||||
|     QSqlQuery dbquery(*db); | ||||
|     dbquery.prepare(prep); | ||||
|     bool success = dbquery.exec(); | ||||
|     QSqlQuery dbQuery = makeSqlQuery(tokenize(query)); | ||||
|     bool success = dbQuery.exec(); | ||||
|     if(!success) | ||||
|     { | ||||
|         qDebug() << "prepped: " << prep; | ||||
|         qDebug() << dbquery.lastError(); | ||||
|         throw QSSGeneralException("SQL Error: " + dbquery.lastError().text()); | ||||
|  | ||||
|         qDebug() << dbQuery.lastError(); | ||||
|         throw QSSGeneralException("SQL Error: " + dbQuery.lastError().text()); | ||||
|     } | ||||
|  | ||||
|     while(dbquery.next()) | ||||
|     while(dbQuery.next()) | ||||
|     { | ||||
|         SearchResult result; | ||||
|         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(); | ||||
|         result.page = dbquery.value("page").toUInt(); | ||||
|         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(); | ||||
|         result.page = dbQuery.value("page").toUInt(); | ||||
|         results.append(result); | ||||
|     } | ||||
|     return results; | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| #ifndef SQLITESEARCH_H | ||||
| #define SQLITESEARCH_H | ||||
| #include <QSqlDatabase> | ||||
| #include <QPair> | ||||
| #include "searchresult.h" | ||||
|  | ||||
| class SqliteSearch | ||||
| @@ -19,18 +20,21 @@ class SqliteSearch | ||||
|  | ||||
|  | ||||
|     }; | ||||
| private: | ||||
|     QSqlDatabase *db; | ||||
|     QVector<SqliteSearch::Token> tokenize(QString expression); | ||||
|     QString createSql(const Token &token); | ||||
|     QString makeSql(const QVector<Token> &tokens); | ||||
|     bool checkParanthesis(QString expression); | ||||
|  | ||||
|  | ||||
| public: | ||||
|  | ||||
|  | ||||
|     SqliteSearch(QSqlDatabase &db); | ||||
|     QVector<SearchResult> search(const QString &query); | ||||
|     static bool checkParanthesis(QString expression); | ||||
|  | ||||
| private: | ||||
|     QSqlDatabase *db; | ||||
|     QVector<Token> tokenize(QString expression); | ||||
|     QSqlQuery makeSqlQuery(const QVector<Token> &tokens); | ||||
|     QString fieldToColumn(QString col); | ||||
|     QPair<QString, QVector<QString> > createSql(const Token &token); | ||||
|     QString createSortSql(const SqliteSearch::Token &token); | ||||
| }; | ||||
|  | ||||
| #endif // SQLITESEARCH_H | ||||
|   | ||||
		Reference in New Issue
	
	Block a user