diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d5185ed..2220344f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/YACReaderLibrary/db/query_lexer.cpp b/YACReaderLibrary/db/query_lexer.cpp index 7b317cdc..c8f65ba7 100644 --- a/YACReaderLibrary/db/query_lexer.cpp +++ b/YACReaderLibrary/db/query_lexer.cpp @@ -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) { diff --git a/YACReaderLibrary/db/query_lexer.h b/YACReaderLibrary/db/query_lexer.h index 4cc2b61f..d3136d0b 100644 --- a/YACReaderLibrary/db/query_lexer.h +++ b/YACReaderLibrary/db/query_lexer.h @@ -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); }; diff --git a/YACReaderLibrary/db/query_parser.cpp b/YACReaderLibrary/db/query_parser.cpp index f9d19e00..654f4aef 100644 --- a/YACReaderLibrary/db/query_parser.cpp +++ b/YACReaderLibrary/db/query_parser.cpp @@ -8,19 +8,25 @@ #include const std::map> 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), {}) }); } diff --git a/YACReaderLibrary/db/query_parser.h b/YACReaderLibrary/db/query_parser.h index 427723c2..db62b127 100644 --- a/YACReaderLibrary/db/query_parser.h +++ b/YACReaderLibrary/db/query_parser.h @@ -9,8 +9,8 @@ #include #include -#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 children; + std::string expOperator; - explicit TreeNode(std::string t, std::vector children) - : t(t), children(children) + explicit TreeNode(std::string t, std::vector 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> fieldNames;