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) | ||||
| { | ||||
| 	ui->setupUi(this); | ||||
| @@ -29,6 +30,8 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi | ||||
| 	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() | ||||
| @@ -129,7 +134,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; | ||||
|   | ||||
| @@ -18,152 +18,6 @@ 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); | ||||
|   | ||||
| @@ -2,6 +2,7 @@ | ||||
| #include <QRegularExpression> | ||||
| #include <QSqlQuery> | ||||
| #include <QSqlError> | ||||
| #include <QStringList> | ||||
| #include <QDebug> | ||||
| #include "sqlitesearch.h" | ||||
| #include "qssgeneralexception.h" | ||||
| @@ -77,89 +78,195 @@ QVector<SqliteSearch::Token> SqliteSearch::tokenize(QString expression) | ||||
| 	return result; | ||||
| } | ||||
|  | ||||
| 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 | ||||
| @@ -18,16 +19,18 @@ 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 | ||||
|   | ||||
		Verwijs in nieuw issue
	
	Block a user