File indexing completed on 2024-04-14 04:35:16

0001 /* This file is part of the KDE project
0002    Copyright (C) 2012-2017 Jarosław Staniek <staniek@kde.org>
0003 
0004    This library is free software; you can redistribute it and/or
0005    modify it under the terms of the GNU Library General Public
0006    License as published by the Free Software Foundation; either
0007    version 2 of the License, or (at your option) any later version.
0008 
0009    This library is distributed in the hope that it will be useful,
0010    but WITHOUT ANY WARRANTY; without even the implied warranty of
0011    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
0012    Library General Public License for more details.
0013 
0014    You should have received a copy of the GNU Library General Public License
0015    along with this library; see the file COPYING.LIB.  If not, write to
0016    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
0017  * Boston, MA 02110-1301, USA.
0018 */
0019 
0020 #include "SqlParserTest.h"
0021 #include <QtTest>
0022 
0023 #include <KDbConnectionData>
0024 #include <KDbDriverManager>
0025 #include <KDbNativeStatementBuilder>
0026 #include <KDbQuerySchema>
0027 #include <KDbToken>
0028 
0029 Q_DECLARE_METATYPE(KDbEscapedString)
0030 
0031 QTEST_GUILESS_MAIN(SqlParserTest)
0032 
0033 void SqlParserTest::initTestCase()
0034 {
0035     QString dir(QFile::decodeName(OUTPUT_DIR));
0036     QString fname("errors.txt");
0037     m_errorFile.setFileName(dir + QDir::separator() + fname);
0038     QVERIFY2(m_errorFile.open(QFile::WriteOnly | QFile::Text),
0039              qPrintable(QString("Cannot open %1 file").arg(m_errorFile.fileName())));
0040     m_errorStream.setDevice(&m_errorFile);
0041 }
0042 
0043 bool SqlParserTest::openDatabase(const QString &path)
0044 {
0045     KDbConnectionOptions options;
0046     options.setReadOnly(true);
0047     if (!m_utils.testConnectAndUse(path, options)) {
0048         return false;
0049     }
0050     m_parser.reset(new KDbParser(m_utils.connection()));
0051 #if 0
0052     if (m_conn->databaseExists(dbName)) {
0053         if (!m_conn->dropDatabase(dbName)) {
0054             m_conn->disconnect();
0055             return false;
0056         }
0057         qDebug() << "Database" << dbName << "dropped.";
0058     }
0059     if (!m_conn->createDatabase(dbName)) {
0060         qDebug() << m_conn->result();
0061         m_conn->disconnect();
0062         return false;
0063     }
0064 #endif
0065     return true;
0066 }
0067 
0068 static void eatComment(QString* string)
0069 {
0070     if (!string->startsWith("--")) {
0071         return;
0072     }
0073     int i = 0;
0074     for (; i < string->length() && string->at(i) == '-'; ++i)
0075         ;
0076     QString result = string->mid(i).trimmed();
0077     *string = result;
0078 }
0079 
0080 static void eatEndLines(QString* string)
0081 {
0082     if (!string->endsWith("--")) {
0083         return;
0084     }
0085     int i = string->length() - 1;
0086     for (; i >= 0 && string->at(i) == '-'; --i)
0087         ;
0088     *string = string->left(i+1).trimmed();
0089 }
0090 
0091 static void eatEndComment(QString* string)
0092 {
0093     int pos = string->indexOf("; --");
0094     if (pos == -1) {
0095         return;
0096     }
0097     string->truncate(pos);
0098     *string = string->trimmed() + ';';
0099 }
0100 
0101 void SqlParserTest::testParse_data()
0102 {
0103     QTest::addColumn<QString>("fname");
0104     QTest::addColumn<int>("lineNum");
0105     QTest::addColumn<KDbEscapedString>("sql");
0106     QTest::addColumn<bool>("expectError");
0107 
0108     QString dir(QFile::decodeName(FILES_DATA_DIR));
0109     QString fname("statements.txt");
0110     QFile input(dir + QDir::separator() + fname);
0111     bool ok = input.open(QFile::ReadOnly | QFile::Text);
0112     QVERIFY2(ok, qPrintable(QString("Could not open data file %1").arg(input.fileName())));
0113     QTextStream in(&input);
0114     QString category;
0115     QString testName;
0116     bool expectError = false;
0117     int lineNum = 1;
0118     QString dbPath;
0119     bool clearTestName = false;
0120 
0121     for (; !in.atEnd(); ++lineNum) {
0122         QString line(in.readLine());
0123         if (line.startsWith("--")) { // comment
0124             eatComment(&line);
0125             eatEndLines(&line);
0126             if (line.startsWith("TODO:")) {
0127                 continue;
0128             }
0129             else if (line.startsWith("CATEGORY: ")) {
0130                 if (clearTestName) {
0131                     expectError = false;
0132                     clearTestName = false;
0133                     testName.clear();
0134                 }
0135                 category = line.mid(QString("CATEGORY: ").length()).trimmed();
0136                 //qDebug() << "CATEGORY:" << category;
0137             }
0138             else if (line == "QUIT") {
0139                 break;
0140             }
0141             else if (line.startsWith("SQLITEFILE: ")) {
0142                 if (clearTestName) {
0143                     expectError = false;
0144                     clearTestName = false;
0145                     testName.clear();
0146                 }
0147                 ok = dbPath.isEmpty();
0148                 QVERIFY2(ok, qPrintable(QString("Error at line %1: SQLite file was already specified (%2)")
0149                     .arg(lineNum).arg(dbPath)));
0150                 dbPath = line.mid(QString("SQLITEFILE: ").length()).trimmed();
0151                 dbPath = dir + QDir::separator() + dbPath;
0152                 ok = openDatabase(dbPath);
0153                 QVERIFY2(ok, qPrintable(QString("Error at line %1: Could not open SQLite file %2")
0154                     .arg(lineNum).arg(dbPath)));
0155             }
0156             else if (line.startsWith("ERROR: ")) {
0157                 if (clearTestName) {
0158                     clearTestName = false;
0159                     testName.clear();
0160                 }
0161                 expectError = true;
0162                 testName = line.mid(QString("ERROR: ").length()).trimmed();
0163             }
0164             else {
0165                 if (clearTestName) {
0166                     expectError = false;
0167                     clearTestName = false;
0168                     testName.clear();
0169                 }
0170                 if (!testName.isEmpty()) {
0171                     testName.append(" ");
0172                 }
0173                 testName.append(line);
0174             }
0175         }
0176         else {
0177             eatEndComment(&line);
0178             KDbEscapedString sql(line.trimmed());
0179             clearTestName = true;
0180             if (sql.isEmpty()) {
0181                 expectError = false;
0182                 continue;
0183             }
0184             ok = !dbPath.isEmpty();
0185             QVERIFY2(ok, qPrintable(QString("Error at line %1: SQLite file was not specified, "
0186                                             "could not execute statement").arg(lineNum)));
0187 
0188             QTest::newRow(qPrintable(QString("file %1:%2, category '%3', test '%4', sql '%5'%6")
0189                           .arg(fname).arg(lineNum).arg(category).arg(testName).arg(sql.toString())
0190                           .arg(expectError ? ", error expected" : "")))
0191                 << fname << lineNum << sql << expectError;
0192         }
0193     }
0194     input.close();
0195 }
0196 
0197 void SqlParserTest::testParse()
0198 {
0199     QFETCH(QString, fname);
0200     QFETCH(int, lineNum);
0201     QFETCH(KDbEscapedString, sql);
0202     QFETCH(bool, expectError);
0203 
0204     QString message;
0205     if (!sql.endsWith(';')) {
0206         message = QString("%1:%2: Missing ';' at the end of line").arg(fname).arg(lineNum);
0207         m_errorStream << fname << ':' << lineNum << ' ' << message << endl;
0208         QVERIFY2(sql.endsWith(';'), qPrintable(message));
0209     }
0210     sql.chop(1);
0211     //qDebug() << "SQL:" << sql.toString() << expectError;
0212 
0213     // 1. Parse
0214     KDbParser *parser = m_parser.data();
0215     bool ok = parser->parse(sql);
0216     QScopedPointer<KDbQuerySchema> query(parser->query());
0217     QCOMPARE(parser->query(), nullptr); // second call should always return nullptr
0218     ok = ok && query;
0219     if (ok) {
0220         // sucess, so error cannot be expected
0221         ok = !expectError;
0222         message = "Unexpected success of parsing SQL statement";
0223         if (!ok) {
0224             m_errorStream << fname << ':' << lineNum << ' ' << message << endl;
0225             if (query) {
0226                 const KDbConnectionAndQuerySchema connQuery(parser->connection(), *query);
0227                 qDebug() << connQuery;
0228                 m_errorStream << KDbUtils::debugString(connQuery) << endl;
0229             }
0230         }
0231         QVERIFY2(ok, qPrintable(message));
0232     }
0233     else {
0234         // failure, so error should be expected
0235         ok = expectError;
0236         message = QString("%1; Failed to parse SQL Statement:\n\"%2\"\n %3^\n")
0237                  .arg(KDbUtils::debugString(parser->error()),
0238                       sql.toString(),
0239                       QString(parser->error().position() - 1, QChar(' ')));
0240         if (ok) {
0241             qDebug() << parser->error();
0242         } else {
0243             m_errorStream << fname << ':' << lineNum << message << endl;
0244         }
0245         QVERIFY2(ok, qPrintable(message));
0246     }
0247 
0248     //! @todo support more drivers
0249     if (query) {
0250         // 2. Build native SQL for SQLite
0251         QList<QVariant> params;
0252         KDbEscapedString querySql;
0253         ok = m_utils.driverBuilder()->generateSelectStatement(&querySql, query.data(), params);
0254         QVERIFY2(ok, "Failed to generate native SQLite SQL statement from query");
0255         //! @todo compare with template
0256     }
0257 
0258     if (query) {
0259         // 3. Build KDbSQL
0260         QList<QVariant> params;
0261         KDbEscapedString querySql;
0262         ok = m_utils.kdbBuilder()->generateSelectStatement(&querySql, query.data(), params);
0263         QVERIFY2(ok, "Failed to generate KDbSQL statement from query");
0264         //! @todo compare with template
0265 
0266         // 3.1. Parse the generated KDbSQL again
0267         ok = parser->parse(querySql);
0268         QScopedPointer<KDbQuerySchema> secondQuery(parser->query());
0269         QCOMPARE(parser->query(), nullptr); // second call should always return nullptr
0270         ok = ok && secondQuery;
0271         QVERIFY2(ok, "Failed to parse generated KDbSQL statement again");
0272 
0273         // 3.2. Compare the original query from step #1 with this query
0274         ok = *query == *secondQuery;
0275         QVERIFY2(ok, "Original query differs from repeatedly parsed query");
0276     }
0277 }
0278 
0279 void SqlParserTest::testTokens()
0280 {
0281     QCOMPARE(KDbToken::SQL_TYPE.value(), 258);
0282     QCOMPARE(KDbToken::AS.value(), 259);
0283     QCOMPARE(KDbToken::AS_EMPTY.value(), 260);
0284     QCOMPARE(KDbToken::ASC.value(), 261);
0285     QCOMPARE(KDbToken::AUTO_INCREMENT.value(), 262);
0286     QCOMPARE(KDbToken::BIT.value(), 263);
0287     QCOMPARE(KDbToken::BITWISE_SHIFT_LEFT.value(), 264);
0288     QCOMPARE(KDbToken::BITWISE_SHIFT_RIGHT.value(), 265);
0289     QCOMPARE(KDbToken::BY.value(), 266);
0290     QCOMPARE(KDbToken::CHARACTER_STRING_LITERAL.value(), 267);
0291     QCOMPARE(KDbToken::CONCATENATION.value(), 268);
0292     QCOMPARE(KDbToken::CREATE.value(), 269);
0293     QCOMPARE(KDbToken::DESC.value(), 270);
0294     QCOMPARE(KDbToken::DISTINCT.value(), 271);
0295     QCOMPARE(KDbToken::DOUBLE_QUOTED_STRING.value(), 272);
0296     QCOMPARE(KDbToken::FROM.value(), 273);
0297     QCOMPARE(KDbToken::JOIN.value(), 274);
0298     QCOMPARE(KDbToken::KEY.value(), 275);
0299     QCOMPARE(KDbToken::LEFT.value(), 276);
0300     QCOMPARE(KDbToken::LESS_OR_EQUAL.value(), 277);
0301     QCOMPARE(KDbToken::GREATER_OR_EQUAL.value(), 278);
0302     QCOMPARE(KDbToken::SQL_NULL.value(), 279);
0303     QCOMPARE(KDbToken::SQL_IS.value(), 280);
0304     QCOMPARE(KDbToken::SQL_IS_NULL.value(), 281);
0305     QCOMPARE(KDbToken::SQL_IS_NOT_NULL.value(), 282);
0306     QCOMPARE(KDbToken::ORDER.value(), 283);
0307     QCOMPARE(KDbToken::PRIMARY.value(), 284);
0308     QCOMPARE(KDbToken::SELECT.value(), 285);
0309     QCOMPARE(KDbToken::INTEGER_CONST.value(), 286);
0310     QCOMPARE(KDbToken::REAL_CONST.value(), 287);
0311     QCOMPARE(KDbToken::RIGHT.value(), 288);
0312     QCOMPARE(KDbToken::SQL_ON.value(), 289);
0313     QCOMPARE(KDbToken::DATE_CONST.value(), 290);
0314     QCOMPARE(KDbToken::DATETIME_CONST.value(), 291);
0315     QCOMPARE(KDbToken::TIME_CONST.value(), 292);
0316     QCOMPARE(KDbToken::TABLE.value(), 293);
0317     QCOMPARE(KDbToken::IDENTIFIER.value(), 294);
0318     QCOMPARE(KDbToken::IDENTIFIER_DOT_ASTERISK.value(), 295);
0319     QCOMPARE(KDbToken::QUERY_PARAMETER.value(), 296);
0320     QCOMPARE(KDbToken::VARCHAR.value(), 297);
0321     QCOMPARE(KDbToken::WHERE.value(), 298);
0322     QCOMPARE(KDbToken::SQL.value(), 299);
0323     QCOMPARE(KDbToken::SQL_TRUE.value(), 300);
0324     QCOMPARE(KDbToken::SQL_FALSE.value(), 301);
0325     QCOMPARE(KDbToken::UNION.value(), 302);
0326     QCOMPARE(KDbToken::SCAN_ERROR.value(), 303);
0327     QCOMPARE(KDbToken::AND.value(), 304);
0328     QCOMPARE(KDbToken::BETWEEN.value(), 305);
0329     QCOMPARE(KDbToken::NOT_BETWEEN.value(), 306);
0330     QCOMPARE(KDbToken::EXCEPT.value(), 307);
0331     QCOMPARE(KDbToken::SQL_IN.value(), 308);
0332     QCOMPARE(KDbToken::INTERSECT.value(), 309);
0333     QCOMPARE(KDbToken::LIKE.value(), 310);
0334     QCOMPARE(KDbToken::ILIKE.value(), 311);
0335     QCOMPARE(KDbToken::NOT_LIKE.value(), 312);
0336     QCOMPARE(KDbToken::NOT.value(), 313);
0337     QCOMPARE(KDbToken::NOT_EQUAL.value(), 314);
0338     QCOMPARE(KDbToken::NOT_EQUAL2.value(), 315);
0339     QCOMPARE(KDbToken::OR.value(), 316);
0340     QCOMPARE(KDbToken::SIMILAR_TO.value(), 317);
0341     QCOMPARE(KDbToken::NOT_SIMILAR_TO.value(), 318);
0342     QCOMPARE(KDbToken::XOR.value(), 319);
0343     QCOMPARE(KDbToken::UMINUS.value(), 320);
0344 
0345     //! @todo add extra tokens: BETWEEN_AND, NOT_BETWEEN_AND
0346 }
0347 
0348 void SqlParserTest::cleanupTestCase()
0349 {
0350     QVERIFY(m_utils.testDisconnect());
0351     m_errorFile.close();
0352 #if 0
0353         if (!m_conn->dropDatabase()) {
0354             qDebug() << m_conn->result();
0355         }
0356         qDebug() << "Database" << m_conn->data().databaseName() << "dropped.";
0357 #endif
0358 }