#include "common/utils_sql.h" #include "common/utils.h" #include "db/sqlquery.h" #include "parser/token.h" #include "parser/lexer.h" #include "parser/keywords.h" #include #include #include #include #include QString invalidIdCharacters = "[](){}\"'@*.,+-=/#$%&|:; \t\n<>"; QHash> wrapperChars; QHash> wrapperEscapedEnding; QList sqlite3Wrappers; QSet sqlite3ReservedLiterals = {"true", "false"}; // true/false as column names - #5065 void initUtilsSql() { wrapperChars[NameWrapper::BRACKET] = QPair('[', ']'); wrapperChars[NameWrapper::QUOTE] = QPair('\'', '\''); wrapperChars[NameWrapper::BACK_QUOTE] = QPair('`', '`'); wrapperChars[NameWrapper::DOUBLE_QUOTE] = QPair('"', '"'); wrapperEscapedEnding[NameWrapper::BRACKET] = QPair(']', false); wrapperEscapedEnding[NameWrapper::QUOTE] = QPair('\'', true); wrapperEscapedEnding[NameWrapper::BACK_QUOTE] = QPair('`', true); wrapperEscapedEnding[NameWrapper::DOUBLE_QUOTE] = QPair('"', true); sqlite3Wrappers << NameWrapper::DOUBLE_QUOTE << NameWrapper::BRACKET << NameWrapper::QUOTE << NameWrapper::BACK_QUOTE; qRegisterMetaType("SqlQueryPtr"); } bool doesObjectNeedWrapping(const QString& str) { if (str.isEmpty()) return true; // It used to return false if object name looked to be wrapped already, // but actually name [abc] is proper name that needs to be wrapped (i.e. "[abc]"). // Bug reported for this was #4362 //if (isObjWrapped(str)) // return false; // The "soft keyword" check added, as they don't require wrapping. // For example: SELECT replace('abc', 'a', '1'); if (isKeyword(str) && !isSoftKeyword(str)) return true; for (int i = 0; i < str.size(); i++) if (doesObjectNeedWrapping(str[i])) return true; if (str[0].isDigit()) return true; if (isReservedLiteral(str)) return true; return false; } bool isReservedLiteral(const QString& str) { return sqlite3ReservedLiterals.contains(str.toLower()); } bool doesObjectNeedWrapping(const QChar& c) { return invalidIdCharacters.indexOf(c) >= 0; } bool isObjectWrapped(const QChar& c) { return !doesObjectNeedWrapping(c); } QString wrapObjIfNeeded(const QString& obj, NameWrapper favWrapper) { if (doesObjectNeedWrapping(obj)) return wrapObjName(obj, favWrapper); return obj; } QString wrapObjIfNeeded(const QString& obj, bool useDoubleQuoteForEmptyValue, NameWrapper favWrapper) { return wrapObjIfNeeded(obj, ((useDoubleQuoteForEmptyValue && obj.isEmpty()) ? NameWrapper::DOUBLE_QUOTE : favWrapper)); } QString wrapObjName(const QString& obj, bool useDoubleQuoteForEmptyValue, NameWrapper favWrapper) { return wrapObjName(obj, ((useDoubleQuoteForEmptyValue && obj.isEmpty()) ? NameWrapper::DOUBLE_QUOTE : favWrapper)); } QString wrapObjName(const QString& obj, NameWrapper favWrapper) { QString result = obj; if (result.isNull()) result = ""; QPair wrapChars = getQuoteCharacter(result, favWrapper); if (wrapChars.first.isNull() || wrapChars.second.isNull()) { qDebug() << "No quote character possible for object name: " << result; return result; } result.prepend(wrapChars.first); result.append(wrapChars.second); return result; } QPair getQuoteCharacter(QString& obj, NameWrapper favWrapper) { QList wrappers = sqlite3Wrappers; // Move favourite wrapper to front of list if (wrappers.contains(favWrapper)) { wrappers.removeOne(favWrapper); wrappers.insert(0, favWrapper); } QPair wrapChars; for (NameWrapper wrapper : wrappers) { wrapChars = wrapperChars[wrapper]; if (obj.indexOf(wrapChars.first) > -1) continue; if (obj.indexOf(wrapChars.second) > -1) continue; return wrapChars; } return QPair(); } QList wrapObjNames(const QList& objList, NameWrapper favWrapper) { QList results; for (int i = 0; i < objList.size(); i++) results << wrapObjName(objList[i], favWrapper); return results; } QList wrapObjNamesIfNeeded(const QList& objList, NameWrapper favWrapper) { QList results; for (int i = 0; i < objList.size(); i++) results << wrapObjIfNeeded(objList[i], favWrapper); return results; } QList getAllNameWrappers() { return {NameWrapper::DOUBLE_QUOTE, NameWrapper::BRACKET, NameWrapper::BACK_QUOTE, NameWrapper::QUOTE}; } QString wrapValueIfNeeded(const QString& str) { return wrapValueIfNeeded(QVariant::fromValue(str)); } QString wrapValueIfNeeded(const QVariant& value) { if (isNumeric(value)) return value.toString(); return wrapString(value.toString()); } QString wrapString(const QString& str) { QString result = str; result.prepend("'"); result.append("'"); return result; } bool doesStringNeedWrapping(const QString& str) { if (str.size() == 0) return false; return str[0] == '\'' && str[str.length()-1] == '\''; } bool isStringWrapped(const QString& str) { return !doesStringNeedWrapping(str); } QString wrapStringIfNeeded(const QString& str) { if (isStringWrapped(str)) return wrapString(str); return str; } QString escapeString(QString& str) { return str.replace('\'', "''"); } QString escapeString(const QString& str) { QString newStr = str; return newStr.replace('\'', "''"); } QString stripString(QString& str) { if (str.length() <= 1) return str; if (str[0] == '\'' && str[str.length()-1] == '\'') return str.mid(1, str.length()-2); return str; } QString stripString(const QString& str) { QString newStr = str; return stripString(newStr); } QString stripEndingSemicolon(const QString& str) { QString newStr = rStrip(str); if (newStr.size() == 0) return str; if (newStr[newStr.size()-1] == ';') { newStr.chop(1); return newStr; } else return str; } QString stripObjName(const QString &str) { QString newStr = str; return stripObjName(newStr); } QString stripObjName(QString &str) { if (str.isNull()) return str; if (str.length() <= 1) return str; if (!isObjWrapped(str)) return str; return str.mid(1, str.length()-2); } bool isObjWrapped(const QString& str) { return getObjWrapper(str) != NameWrapper::null; } bool doesNotContainEndingWrapperChar(const QString& str, NameWrapper wrapper) { QString innerPart = str.mid(1, str.length() - 2); const QChar& endingChar = wrapperEscapedEnding[wrapper].first; bool escapingAllowed = wrapperEscapedEnding[wrapper].second; int idx = -1; int lastIdx = innerPart.length() - 1; while ((idx = innerPart.indexOf(endingChar, idx + 1)) > -1) { if (idx == lastIdx || !escapingAllowed || innerPart[idx + 1] != endingChar) return false; idx++; // we had occurrence, but it was escaped, so we need to skip the second (escape) char } return true; } NameWrapper getObjWrapper(const QString& str) { if (str.isEmpty()) return NameWrapper::null; for (NameWrapper wrapper : sqlite3Wrappers) { QPair chars = wrapperChars[wrapper]; if (str[0] == chars.first && str[str.length()-1] == chars.second && doesNotContainEndingWrapperChar(str, wrapper)) return wrapper; } return NameWrapper::null; } bool isWrapperChar(const QChar& c) { for (NameWrapper wrapper : sqlite3Wrappers) { QPair chars = wrapperChars[wrapper]; if (c == chars.first || c == chars.second) return true; } return false; } int qHash(NameWrapper wrapper) { return (uint)wrapper; } QString getPrefixDb(const QString& origDbName) { if (origDbName.isEmpty()) return "main"; else return wrapObjIfNeeded(origDbName); } bool isSystemTable(const QString &name) { return name.startsWith("sqlite_"); } bool isSystemIndex(const QString &name) { return name.startsWith("sqlite_autoindex_"); } TokenPtr stripObjName(TokenPtr token) { if (!token) return token; token->value = stripObjName(token->value); return token; } QString removeComments(const QString& value) { Lexer lexer; TokenList tokens = lexer.tokenize(value); while (tokens.remove(Token::COMMENT)) continue; return tokens.detokenize(); } void splitQueriesUpdateCaseWhenDepth(Token::Type type, const QString& value, int& caseWhenDepth) { if (type != Token::KEYWORD) return; if (value == "CASE") caseWhenDepth++; else if (value == "END" && caseWhenDepth > 0) caseWhenDepth--; } QList splitQueries(const TokenList& tokenizedQuery, bool* complete) { QList queries; TokenList currentQueryTokens; QString value; int caseWhenDepth = 0; int createTriggerMeter = 0; bool insideTrigger = false; bool completeQuery = false; for (const TokenPtr& token : tokenizedQuery) { value = token->value.toUpper(); if (!token->isWhitespace()) completeQuery = false; if (insideTrigger) { if (token->type == Token::KEYWORD && value == "END" && caseWhenDepth <= 0 && caseWhenDepth == 0) { insideTrigger = false; completeQuery = true; } currentQueryTokens << token; splitQueriesUpdateCaseWhenDepth(token->type, value, caseWhenDepth); continue; } splitQueriesUpdateCaseWhenDepth(token->type, value, caseWhenDepth); if (token->type == Token::KEYWORD) { if (value == "CREATE" || value == "TRIGGER" || value == "BEGIN") createTriggerMeter++; if (createTriggerMeter == 3) insideTrigger = true; currentQueryTokens << token; } else if (token->type == Token::OPERATOR && value == ";") { createTriggerMeter = 0; caseWhenDepth = 0; currentQueryTokens << token; queries << currentQueryTokens; currentQueryTokens.clear(); completeQuery = true; } else { currentQueryTokens << token; } } if (currentQueryTokens.size() > 0) queries << currentQueryTokens; if (complete) *complete = completeQuery; return queries; } QStringList quickSplitQueries(const QString& sql, bool keepEmptyQueries, bool removeComments) { QChar c; bool inString = false; bool inMultiLineComment = false; bool inSingleLineComment = false; QStringList queries; QString query; QString trimmed; for (int i = 0, total = sql.size(); i < total; ++i) { c = sql[i]; // String if (inString) { query += c; if (c == '\'') { inString = false; } continue; } // One-line comment if (inSingleLineComment) { if (!removeComments) query += c; if (c == '\r' && (i + 1) < total && sql[i+1] == '\n') { if (!removeComments) query += '\n'; i++; inSingleLineComment = false; } else if (c == '\n' || c == '\r') inSingleLineComment = false; continue; } // Multi-line comment if (inMultiLineComment) { if (!removeComments) query += c; if (c == '*' && (i + 1) < total && sql[i+1] == '/') { if (!removeComments) query += '/'; i++; inMultiLineComment = false; } continue; } // Everything rest if (c == '\'') { query += c; inString = true; } else if (c == '-' && (i + 1) < total && sql[i+1] == '-') { inSingleLineComment = true; i++; if (!removeComments) query += "--"; } else if (c == '/' && (i + 1) < total && sql[i+1] == '*') { inMultiLineComment = true; i++; if (!removeComments) query += "/*"; } else if (c == ';') { query += c; if (keepEmptyQueries || (!(trimmed = query.trimmed()).isEmpty() && trimmed != ";")) queries << query; query.clear(); } else { query += c; } } if (!query.isNull() && (!(trimmed = query.trimmed()).isEmpty() && trimmed != ";")) queries << query; return queries; } QStringList splitQueries(const QString& sql, bool keepEmptyQueries, bool removeComments, bool* complete) { TokenList tokens = Lexer::tokenize(sql); if (removeComments) tokens = tokens.filterOut(Token::COMMENT); QList tokenizedQueries = splitQueries(tokens, complete); QString query; QStringList queries; for (const TokenList& queryTokens : tokenizedQueries) { query = queryTokens.detokenize(); if (keepEmptyQueries || (!query.trimmed().isEmpty() && query.trimmed() != ";")) queries << query; } return queries; } QString getQueryWithPosition(const QStringList& queries, int position, int* startPos) { int currentPos = 0; int length = 0; if (startPos) *startPos = 0; for (const QString& query : queries) { length = query.length(); if (position >= currentPos && position < currentPos+length) return query; currentPos += length; if (startPos) *startPos += length; } // If we passed all queries and it happens that the cursor is just after last query - this is the query we want. if (position == currentPos && queries.size() > 0) { if (startPos) *startPos -= length; return queries.last(); } if (startPos) *startPos = -1; return QString(); } int getCursorFinalPositionForQueries(const QString& queries, int position) { const static QSet whitespaceChars = {' ', '\t'}; int len = queries.length(); if (position >= len || position < 1) return position; if (!whitespaceChars.contains(queries[position]) && queries[position] != '\xa') return position; // First checking characters at & after cursor - are they just whitespaces until end of line? int newPos = position; while (newPos < len) { QChar c = queries[newPos++]; if (whitespaceChars.contains(c)) continue; else if (c == '\xa') break; else return position; } // Okay, only whitespaces after cursor, so let's check whats before cursor, // until the ';' - is it just whitespaces too? newPos = position; while (newPos > 1 && whitespaceChars.contains(queries[newPos - 1])) newPos--; if (queries[newPos - 1] == ';') { // Jackpot! Our cursor is just somewhere in whitespaces after the query. // Let's consider that prior query as current. return newPos - 1; } return position; } QString getQueryWithPosition(const QString& queries, int position, int* startPos) { position = getCursorFinalPositionForQueries(queries, position); QStringList queryList = splitQueries(queries); return getQueryWithPosition(queryList, position, startPos); } QPair getQueryBoundriesForPosition(const QString& contents, int cursorPosition, bool fallBackToPreviousIfNecessary) { int queryStartPos; QString query = getQueryWithPosition(contents, cursorPosition, &queryStartPos); TokenList tokens = Lexer::tokenize(query); tokens.trim(); tokens.trimRight(Token::OPERATOR, ";"); if (tokens.size() == 0 && fallBackToPreviousIfNecessary) { // Fallback cursorPosition = contents.lastIndexOf(";", cursorPosition - 1); if (cursorPosition > -1) { query = getQueryWithPosition(contents, cursorPosition, &queryStartPos); tokens = Lexer::tokenize(query); tokens.trim(); tokens.trimRight(Token::OPERATOR, ";"); } } if (tokens.size() == 0) return QPair(-1, -1); return QPair(tokens.first()->start + queryStartPos, tokens.last()->end + 1 + queryStartPos); } QString trimBindParamPrefix(const QString& param) { if (param == "?") return param; if (param.startsWith("$") || param.startsWith("@") || param.startsWith(":") || param.startsWith("?")) return param.mid(1); return param; } QList getQueriesWithParamNames(const QString& query) { QList results; TokenList allTokens = Lexer::tokenize(query); QList queries = splitQueries(allTokens); QString queryStr; QStringList paramNames; for (const TokenList& tokens : queries) { paramNames.clear(); for (const TokenPtr& token : tokens.filter(Token::BIND_PARAM)) paramNames << token->value; queryStr = tokens.detokenize().trimmed(); if (!queryStr.isEmpty()) results << QueryWithParamNames(queryStr, paramNames); } return results; } QList getQueriesWithParamCount(const QString& query) { QList results; TokenList allTokens = Lexer::tokenize(query); QList queries = splitQueries(allTokens); QString queryStr; for (const TokenList& tokens : queries) { queryStr = tokens.detokenize().trimmed(); if (!queryStr.isEmpty()) results << QueryWithParamCount(queryStr, tokens.filter(Token::BIND_PARAM).size()); } return results; } QueryWithParamNames getQueryWithParamNames(const QString& query) { TokenList allTokens = Lexer::tokenize(query); QStringList paramNames; for (const TokenPtr& token : allTokens.filter(Token::BIND_PARAM)) paramNames << token->value; return QueryWithParamNames(query, paramNames); } QueryWithParamCount getQueryWithParamCount(const QString& query) { TokenList allTokens = Lexer::tokenize(query); return QueryWithParamCount(query, allTokens.filter(Token::BIND_PARAM).size()); } QString commentAllSqlLines(const QString& sql) { QStringList lines = splitByLines(sql); QMutableStringListIterator it(lines); while (it.hasNext()) it.next().prepend("-- "); return joinLines(lines); } QString getBindTokenName(const TokenPtr& token) { if (token->type != Token::BIND_PARAM) return QString(); if (token->value == "?") return token->value; return token->value.mid(1); } QueryAccessMode getQueryAccessMode(const QString& query, bool* isSelect) { static QStringList readOnlyCommands = {"ANALYZE", "EXPLAIN", "PRAGMA", "SELECT"}; if (isSelect) *isSelect = false; TokenList tokens = Lexer::tokenize(query); int keywordIdx = tokens.indexOf(Token::KEYWORD); if (keywordIdx < 0) return QueryAccessMode::WRITE; int cmdIdx = readOnlyCommands.indexOf(tokens[keywordIdx]->value.toUpper()); if (keywordIdx > -1 && cmdIdx > -1) { if (cmdIdx == 3 && isSelect) *isSelect = true; return QueryAccessMode::READ; } if (keywordIdx > -1 && tokens[keywordIdx]->value.toUpper() == "WITH") { bool matched = false; bool queryIsSelect = false; int depth = 0; for (TokenPtr token : tokens) { switch (token->type) { case Token::PAR_LEFT: depth++; break; case Token::PAR_RIGHT: depth--; break; case Token::KEYWORD: if (depth == 0) { QString val = token->value.toUpper(); if (val == "SELECT") { matched = true; queryIsSelect = true; } else if (val == "DELETE" || val == "UPDATE" || val == "INSERT") { matched = true; } } break; default: break; } if (matched) break; } if (queryIsSelect) { if (isSelect) *isSelect = true; return QueryAccessMode::READ; } } return QueryAccessMode::WRITE; } QStringList valueListToSqlList(const QVariantList& values) { QStringList argList; for (const QVariant& value : values) argList << valueToSqlLiteral(value); return argList; } QString valueToSqlLiteral(const QVariant& value) { if (!value.isValid() || value.isNull()) return "NULL"; switch (value.userType()) { case QVariant::Int: case QVariant::UInt: case QVariant::LongLong: case QVariant::ULongLong: return value.toString(); case QVariant::Double: return doubleToString(value); case QVariant::Bool: return QString::number(value.toInt()); case QVariant::ByteArray: return "X'" + value.toByteArray().toHex().toUpper() + "'"; default: break; } return wrapString(escapeString(value.toString())); } QStringList wrapStrings(const QStringList& strList) { QStringList list; for (const QString& str : strList) list << wrapString(str); return list; } QString trimQueryEnd(const QString &query) { QString q = query.trimmed(); while (q.endsWith(";")) { q.chop(1); q = q.trimmed(); } return q; } SqliteDataType toSqliteDataType(const QString& typeStr) { QString upperType = typeStr.trimmed().toUpper(); if (upperType == "INTEGER") return SqliteDataType::INTEGER; if (upperType == "REAL") return SqliteDataType::REAL; if (upperType == "TEXT") return SqliteDataType::TEXT; if (upperType == "BLOB") return SqliteDataType::BLOB; if (upperType == "NULL") return SqliteDataType::_NULL; return SqliteDataType::UNKNOWN; } QByteArray blobFromLiteral(const QString& value) { if (value.length() <= 3) { qCritical() << "Call to blobFromLiteral() with blob literal shorter or equal to 3 characters:" << value; return QByteArray(); } QString hex = value.mid(2, value.length() - 3); return QByteArray::fromHex(hex.toLatin1()); }