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 ( ) )
{
QString & lastKey = result . last ( ) . key ;
if ( ! isBool ( lastKey ) & & ! isSort ( lastKey ) & & ! isSort ( filtername ) )
{
result . append ( Token ( " AND " ) ) ;
}
}
2019-04-20 23:31:14 +02:00
if ( bracket ! = " " )
{
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
result . append ( Token ( " AND " ) ) ;
2019-04-20 23:31:14 +02:00
}
2019-05-04 20:40:43 +02:00
result . append ( Token ( bracket ) ) ;
2019-04-20 23:31:14 +02:00
}
if ( loneword ! = " " )
{
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 )
{
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 ) ;
}
}
else
{
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 )
{
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 " " ;
}
2019-04-20 23:31:14 +02:00
2019-04-25 10:27:54 +02:00
QPair < QString , QVector < QString > > SqliteSearch : : createSql ( const SqliteSearch : : Token & token )
2019-04-20 23:31:14 +02:00
{
2019-04-25 10:27:54 +02:00
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-27 21:23:06 +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 " + 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 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-20 23:31:14 +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 ;
}