mirror of
https://github.com/YACReader/yacreader
synced 2025-05-28 03:10:27 -04:00
Improve search engine with new operators and new fields
This commit is contained in:
parent
117d65ffbe
commit
6d457be912
@ -10,6 +10,8 @@ Version counting is based on semantic versioning (Major.Feature.Patch)
|
||||
* Add support for showing a "recently added/updated" indicator.
|
||||
* Improved comic metadata dialog.
|
||||
* Add textual tags support that can be queried through the search engine.
|
||||
* Make = in the search engine work as : does.
|
||||
* Add new operators to the search engine: exact match ==, <, >, <=, >=.
|
||||
|
||||
## 9.12
|
||||
|
||||
|
@ -17,6 +17,14 @@ Token QueryLexer::next()
|
||||
case '(':
|
||||
case ')':
|
||||
return single(Token::Type::opcode);
|
||||
case ':':
|
||||
return single(Token::Type::equal);
|
||||
case '=':
|
||||
return equal();
|
||||
case '<':
|
||||
return minor();
|
||||
case '>':
|
||||
return major();
|
||||
case '"':
|
||||
return quotedWord();
|
||||
default:
|
||||
@ -44,7 +52,7 @@ Token QueryLexer::word()
|
||||
auto start = index;
|
||||
get();
|
||||
auto current = peek();
|
||||
while (current != '\0' && !isSpace(current) && current != '"' && current != '(' && current != ')') {
|
||||
while (current != '\0' && !isSpace(current) && current != '"' && current != '(' && current != ')' && current != ':' && current != '=' && current != '<' && current != '>') {
|
||||
get();
|
||||
current = peek();
|
||||
}
|
||||
@ -70,6 +78,45 @@ Token QueryLexer::quotedWord()
|
||||
return Token(Token::Type::eof);
|
||||
}
|
||||
|
||||
Token QueryLexer::minor()
|
||||
{
|
||||
auto start = index;
|
||||
get();
|
||||
auto current = peek();
|
||||
if (current == '=') {
|
||||
get();
|
||||
return Token(Token::Type::minorOrEqual, input.substr(start, index - start));
|
||||
}
|
||||
|
||||
return Token(Token::Type::minor, input.substr(start, index - start));
|
||||
}
|
||||
|
||||
Token QueryLexer::major()
|
||||
{
|
||||
auto start = index;
|
||||
get();
|
||||
auto current = peek();
|
||||
if (current == '=') {
|
||||
get();
|
||||
return Token(Token::Type::majorOrEqual, input.substr(start, index - start));
|
||||
}
|
||||
|
||||
return Token(Token::Type::major, input.substr(start, index - start));
|
||||
}
|
||||
|
||||
Token QueryLexer::equal()
|
||||
{
|
||||
auto start = index;
|
||||
get();
|
||||
auto current = peek();
|
||||
if (current == '=') {
|
||||
get();
|
||||
return Token(Token::Type::exactEqual, input.substr(start, index - start));
|
||||
}
|
||||
|
||||
return Token(Token::Type::equal, input.substr(start, index - start));
|
||||
}
|
||||
|
||||
bool QueryLexer::isSpace(char c)
|
||||
{
|
||||
switch (c) {
|
||||
|
@ -11,6 +11,12 @@ public:
|
||||
opcode,
|
||||
word,
|
||||
quotedWord,
|
||||
equal, // =
|
||||
exactEqual, // ==
|
||||
minor,
|
||||
major,
|
||||
minorOrEqual,
|
||||
majorOrEqual,
|
||||
undefined
|
||||
};
|
||||
|
||||
@ -50,6 +56,9 @@ private:
|
||||
Token single(Token::Type type);
|
||||
Token word();
|
||||
Token quotedWord();
|
||||
Token minor();
|
||||
Token major();
|
||||
Token equal();
|
||||
|
||||
bool isSpace(char c);
|
||||
};
|
||||
|
@ -8,19 +8,25 @@
|
||||
#include <QsLog.h>
|
||||
|
||||
const std::map<QueryParser::FieldType, std::vector<std::string>> QueryParser::fieldNames {
|
||||
{ FieldType::numeric, { "numpages", "count", "arccount", "alternateCount" } },
|
||||
// TODO_METADATA support dates
|
||||
{ FieldType::numeric, { "numpages", "count", "arccount", "alternateCount", "rating" } },
|
||||
{ FieldType::text, { "number", "arcnumber", "title", "volume", "storyarc", "genere", "writer", "penciller", "inker", "colorist", "letterer", "coverartist", "publisher", "format", "agerating", "synopsis", "characters", "notes", "editor", "imprint", "teams", "locations", "series", "alternateSeries", "alternateNumber", "languageISO", "seriesGroup", "mainCharacterOrTeam", "review", "tags" } },
|
||||
{ FieldType::boolean, { "isbis", "color", "read" } },
|
||||
{ FieldType::boolean, { "color", "read", "edited", "hasBeenOpened" } },
|
||||
{ FieldType::date, { "date", "added", "lastTimeOpened" } },
|
||||
{ FieldType::filename, { "filename" } },
|
||||
{ FieldType::folder, { "folder" } },
|
||||
{ FieldType::booleanFolder, { "completed", "finished" } }, // TODO_METADTA include new folder fields, e.g. type
|
||||
{ FieldType::enumField, { "type" } }
|
||||
{ FieldType::enumField, { "type" } },
|
||||
{ FieldType::enumFieldFolder, { "foldertype" } }
|
||||
};
|
||||
|
||||
int QueryParser::TreeNode::buildSqlString(std::string &sqlString, int bindPosition) const
|
||||
{
|
||||
if (t == "token") {
|
||||
// TODO: add some semantic checks, not all operators apply to all fields
|
||||
|
||||
// TODO: add support for == for an exact comparison
|
||||
// TODO: try to add support for <,>,<=,>= for number, even if it's a string now maybe it can be done
|
||||
if (t == "expression") {
|
||||
++bindPosition;
|
||||
if (toLower(children[0].t) == "all") {
|
||||
sqlString += "(";
|
||||
@ -29,7 +35,15 @@ int QueryParser::TreeNode::buildSqlString(std::string &sqlString, int bindPositi
|
||||
}
|
||||
sqlString += "UPPER(c.filename) LIKE UPPER(:bindPosition" + std::to_string(bindPosition) + ") OR ";
|
||||
sqlString += "UPPER(f.name) LIKE UPPER(:bindPosition" + std::to_string(bindPosition) + ")) ";
|
||||
} else if (isIn(fieldType(children[0].t), { FieldType::numeric, FieldType::boolean, FieldType::enumField })) {
|
||||
} else if (isIn(fieldType(children[0].t), { FieldType::numeric })) {
|
||||
std::string sqlOperator;
|
||||
if (expOperator == ":" || expOperator == "=" || expOperator == "==") {
|
||||
sqlOperator = "=";
|
||||
} else {
|
||||
sqlOperator = expOperator;
|
||||
}
|
||||
sqlString += "ci." + children[0].t + " " + sqlOperator + " :bindPosition" + std::to_string(bindPosition) + " ";
|
||||
} else if (isIn(fieldType(children[0].t), { FieldType::boolean, FieldType::enumField })) {
|
||||
sqlString += "ci." + children[0].t + " = :bindPosition" + std::to_string(bindPosition) + " ";
|
||||
} else if (fieldType(children[0].t) == FieldType::filename) {
|
||||
sqlString += "(UPPER(c." + children[0].t + ") LIKE UPPER(:bindPosition" + std::to_string(bindPosition) + ")) ";
|
||||
@ -37,8 +51,23 @@ int QueryParser::TreeNode::buildSqlString(std::string &sqlString, int bindPositi
|
||||
sqlString += "(UPPER(f.name) LIKE UPPER(:bindPosition" + std::to_string(bindPosition) + ")) ";
|
||||
} else if (fieldType(children[0].t) == FieldType::booleanFolder) {
|
||||
sqlString += "f." + children[0].t + " = :bindPosition" + std::to_string(bindPosition) + " ";
|
||||
} else if (fieldType(children[0].t) == FieldType::enumFieldFolder) {
|
||||
if (children[0].t == "foldertype") {
|
||||
sqlString += "f.type = :bindPosition" + std::to_string(bindPosition) + " ";
|
||||
} else {
|
||||
sqlString += "f." + children[0].t + " = :bindPosition" + std::to_string(bindPosition) + " ";
|
||||
}
|
||||
} else {
|
||||
sqlString += "(UPPER(ci." + children[0].t + ") LIKE UPPER(:bindPosition" + std::to_string(bindPosition) + ")) ";
|
||||
if (expOperator == "=" || expOperator == ":" || expOperator == "") {
|
||||
sqlString += "(UPPER(ci." + children[0].t + ") LIKE UPPER(:bindPosition" + std::to_string(bindPosition) + ")) ";
|
||||
} else {
|
||||
if (expOperator == "==") {
|
||||
sqlString += "(UPPER(ci." + children[0].t + ") = UPPER(:bindPosition" + std::to_string(bindPosition) + ")) ";
|
||||
} else {
|
||||
// support for <,>,<=,>= in text fields makes sense for number, arcNumber, alternateNumber, but (TODO) the syntax won't prevent other fields from using this operators
|
||||
sqlString += "(CAST(ci." + children[0].t + " as REAL) " + expOperator + " CAST(:bindPosition" + std::to_string(bindPosition) + " as REAL)) ";
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (t == "not") {
|
||||
sqlString += "(NOT ";
|
||||
@ -57,7 +86,7 @@ int QueryParser::TreeNode::buildSqlString(std::string &sqlString, int bindPositi
|
||||
|
||||
int QueryParser::TreeNode::bindValues(QSqlQuery &selectQuery, int bindPosition) const
|
||||
{
|
||||
if (t == "token") {
|
||||
if (t == "expression") {
|
||||
std::string bind_string(":bindPosition" + std::to_string(++bindPosition));
|
||||
if (isIn(fieldType(children[0].t), { FieldType::numeric })) {
|
||||
selectQuery.bindValue(QString::fromStdString(bind_string), std::stoi(children[1].t));
|
||||
@ -70,10 +99,10 @@ int QueryParser::TreeNode::bindValues(QSqlQuery &selectQuery, int bindPosition)
|
||||
} else {
|
||||
selectQuery.bindValue(QString::fromStdString(bind_string), std::stoi(value));
|
||||
}
|
||||
} else if ((isIn(fieldType(children[0].t), { FieldType::enumField }))) {
|
||||
} else if ((isIn(fieldType(children[0].t), { FieldType::enumField, FieldType::enumFieldFolder }))) {
|
||||
auto enumType = children[0].t;
|
||||
auto value = toLower(children[1].t);
|
||||
if (enumType == "type") {
|
||||
if (enumType == "type" || enumType == "foldertype") {
|
||||
if (value == "comic") {
|
||||
selectQuery.bindValue(QString::fromStdString(bind_string), 0);
|
||||
} else if (value == "manga") {
|
||||
@ -89,7 +118,11 @@ int QueryParser::TreeNode::bindValues(QSqlQuery &selectQuery, int bindPosition)
|
||||
selectQuery.bindValue(QString::fromStdString(bind_string), std::stoi(children[1].t));
|
||||
}
|
||||
} else {
|
||||
selectQuery.bindValue(QString::fromStdString(bind_string), QString::fromStdString("%%" + children[1].t + "%%"));
|
||||
if (expOperator == "=" || expOperator == ":" || expOperator == "") {
|
||||
selectQuery.bindValue(QString::fromStdString(bind_string), QString::fromStdString("%%" + children[1].t + "%%"));
|
||||
} else {
|
||||
selectQuery.bindValue(QString::fromStdString(bind_string), QString::fromStdString(children[1].t));
|
||||
}
|
||||
}
|
||||
} else if (t == "not") {
|
||||
bindPosition = children[0].bindValues(selectQuery, bindPosition);
|
||||
@ -172,6 +205,16 @@ void QueryParser::advance()
|
||||
currentToken = lexer.next();
|
||||
}
|
||||
|
||||
bool QueryParser::isOperatorToken(Token::Type type)
|
||||
{
|
||||
return type == Token::Type::equal ||
|
||||
type == Token::Type::exactEqual ||
|
||||
type == Token::Type::minor ||
|
||||
type == Token::Type::minorOrEqual ||
|
||||
type == Token::Type::major ||
|
||||
type == Token::Type::majorOrEqual;
|
||||
}
|
||||
|
||||
QueryParser::FieldType QueryParser::fieldType(const std::string &str)
|
||||
{
|
||||
for (const auto &names : fieldNames) {
|
||||
@ -244,26 +287,39 @@ QueryParser::TreeNode QueryParser::locationExpression()
|
||||
if (!isIn(tokenType(), { Token::Type::word, Token::Type::quotedWord })) {
|
||||
throw std::invalid_argument("Invalid syntax. Expected a lookup name or a word");
|
||||
}
|
||||
|
||||
return expression();
|
||||
}
|
||||
|
||||
QueryParser::TreeNode QueryParser::expression()
|
||||
{
|
||||
if (tokenType() == Token::Type::word) {
|
||||
auto left = token(true);
|
||||
if (isOperatorToken(tokenType())) {
|
||||
auto expOperator = token(true);
|
||||
if (tokenType() != Token::Type::word && tokenType() != Token::Type::quotedWord) {
|
||||
throw std::invalid_argument("missing right operand");
|
||||
}
|
||||
auto right = token(true);
|
||||
|
||||
return TreeNode("expression", { TreeNode(toLower(left), {}), TreeNode(right, {}) }, expOperator);
|
||||
} else {
|
||||
return TreeNode("expression", { TreeNode("all", {}), TreeNode(left, {}) });
|
||||
}
|
||||
}
|
||||
|
||||
return baseToken();
|
||||
}
|
||||
|
||||
QueryParser::TreeNode QueryParser::baseToken()
|
||||
{
|
||||
if (tokenType() == Token::Type::quotedWord) {
|
||||
return TreeNode("token", { TreeNode("all", {}), TreeNode(token(true), {}) });
|
||||
return TreeNode("expression", { TreeNode("all", {}), TreeNode(token(true), {}) });
|
||||
}
|
||||
|
||||
// TODO ":" should come from the lexer as a token
|
||||
auto words(split(token(true), ':'));
|
||||
|
||||
if (words.size() > 1 && fieldType(words[0].toStdString()) != FieldType::unknown) {
|
||||
auto loc(toLower(words[0].toStdString()));
|
||||
words.erase(words.begin());
|
||||
if (words.size() == 1 && tokenType() == Token::Type::quotedWord) {
|
||||
return TreeNode("token", { TreeNode(loc, {}), TreeNode(token(true), {}) });
|
||||
}
|
||||
return TreeNode("token", { TreeNode(loc, {}), TreeNode(join(words, ":"), {}) });
|
||||
if (tokenType() == Token::Type::word) {
|
||||
return TreeNode("expression", { TreeNode("all", {}), TreeNode(token(true), {}) });
|
||||
}
|
||||
|
||||
return TreeNode("token", { TreeNode("all", {}), TreeNode(join(words, ":"), {}) });
|
||||
return TreeNode("expression", { TreeNode("all", {}), TreeNode(token(true), {}) });
|
||||
}
|
||||
|
@ -9,8 +9,8 @@
|
||||
#include <vector>
|
||||
#include <list>
|
||||
|
||||
#define SEARCH_FOLDERS_QUERY "SELECT DISTINCT f.id, f.parentId, f.name, f.path, f.finished, f.completed, f.numChildren, f.firstChildHash FROM folder f LEFT JOIN comic c ON (f.id = c.parentId) INNER JOIN comic_info ci ON (c.comicInfoId = ci.id) WHERE "
|
||||
#define SEARCH_COMICS_QUERY "SELECT ci.number,ci.title,c.fileName,ci.numPages,c.id,c.parentId,c.path,ci.hash,ci.read,ci.isBis,ci.currentPage,ci.rating,ci.hasBeenOpened,ci.coverSizeRatio,ci.lastTimeOpened,ci.manga FROM comic c INNER JOIN comic_info ci ON (c.comicInfoId = ci.id) LEFT JOIN folder f ON (f.id == c.parentId) WHERE "
|
||||
#define SEARCH_FOLDERS_QUERY "SELECT DISTINCT * FROM folder f LEFT JOIN comic c ON (f.id = c.parentId) INNER JOIN comic_info ci ON (c.comicInfoId = ci.id) WHERE "
|
||||
#define SEARCH_COMICS_QUERY "SELECT ci.number,ci.title,c.fileName,ci.numPages,c.id,c.parentId,c.path,ci.hash,ci.read,ci.isBis,ci.currentPage,ci.rating,ci.hasBeenOpened,ci.date,ci.added,ci.type FROM comic c INNER JOIN comic_info ci ON (c.comicInfoId = ci.id) LEFT JOIN folder f ON (f.id == c.parentId) WHERE "
|
||||
|
||||
/**
|
||||
* This class is used to generate an SQL query string from a search expression,
|
||||
@ -25,7 +25,9 @@
|
||||
* and_expression ::= not_expression [ [ 'and' ] and_expression ]
|
||||
* not_expression ::= [ 'not' ] location_expression
|
||||
* location_expression ::= base_token | ( '(' or_expression ')' )
|
||||
* base_token ::= a sequence of letters and colons, perhaps quoted
|
||||
* expression :: base_token | base_token 'operator' base_token
|
||||
* operator :: [':' '=' '<' '>' '<=' '=>']
|
||||
* base_token ::= a sequence of letters, perhaps quoted
|
||||
*
|
||||
* Usage Example:
|
||||
* QSqlQuery selectQuery(db);
|
||||
@ -47,9 +49,10 @@ public:
|
||||
struct TreeNode {
|
||||
std::string t;
|
||||
std::vector<TreeNode> children;
|
||||
std::string expOperator;
|
||||
|
||||
explicit TreeNode(std::string t, std::vector<TreeNode> children)
|
||||
: t(t), children(children)
|
||||
explicit TreeNode(std::string t, std::vector<TreeNode> children, std::string expOperator = "")
|
||||
: t(t), children(children), expOperator(expOperator)
|
||||
{
|
||||
}
|
||||
|
||||
@ -78,6 +81,8 @@ private:
|
||||
return std::find(v.begin(), v.end(), e) != v.end();
|
||||
}
|
||||
|
||||
bool isOperatorToken(Token::Type type);
|
||||
|
||||
enum class FieldType { unknown,
|
||||
numeric,
|
||||
text,
|
||||
@ -86,7 +91,8 @@ private:
|
||||
folder,
|
||||
booleanFolder,
|
||||
filename,
|
||||
enumField };
|
||||
enumField,
|
||||
enumFieldFolder };
|
||||
static FieldType fieldType(const std::string &str);
|
||||
|
||||
static std::string join(const QStringList &strings, const std::string &delim);
|
||||
@ -96,6 +102,7 @@ private:
|
||||
TreeNode andExpression();
|
||||
TreeNode notExpression();
|
||||
TreeNode locationExpression();
|
||||
TreeNode expression();
|
||||
TreeNode baseToken();
|
||||
|
||||
static const std::map<FieldType, std::vector<std::string>> fieldNames;
|
||||
|
Loading…
Reference in New Issue
Block a user