Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion src/RunSql.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -319,10 +319,34 @@ void RunSql::stopExecution()
queries_left_to_execute.clear();
}

RunSql::StatementType RunSql::getQueryType(const QString& query)
RunSql::StatementType RunSql::getQueryType(const QString& query_with_comments)
{
// Helper function for getting the type of a given query

// The text of a statement as handed out by SQLite includes any comments in front of it. Skip them, otherwise
// something like "-- compact\nVACUUM;" isn't recognised as what it is.
int start = 0;
for(;;)
{
while(start < query_with_comments.length() && query_with_comments.at(start).isSpace())
start++;

if(query_with_comments.mid(start, 2) == QLatin1String("--"))
{
start = query_with_comments.indexOf('\n', start);
if(start == -1)
return OtherStatement;
} else if(query_with_comments.mid(start, 2) == QLatin1String("/*")) {
start = query_with_comments.indexOf(QLatin1String("*/"), start + 2);
if(start == -1)
return OtherStatement;
start += 2;
} else {
break;
}
}
const QString query = query_with_comments.mid(start);

if(query.startsWith("SELECT", Qt::CaseInsensitive)) return SelectStatement;
if(query.startsWith("ALTER", Qt::CaseInsensitive)) return AlterStatement;
if(query.startsWith("DROP", Qt::CaseInsensitive)) return DropStatement;
Expand Down
51 changes: 51 additions & 0 deletions src/tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,54 @@ set(TESTCACHE_HDR
add_executable(test-cache ${TESTCACHE_HDR} ${TESTCACHE_SRC})
target_link_libraries(test-cache ${QT_MAJOR}::Test)
add_test(NAME test-cache COMMAND $<TARGET_FILE:test-cache>)

# test run sql

set(TESTRUNSQL_SRC
TestRunSql.cpp
../RunSql.cpp
../sqlitedb.cpp
../sqlitetablemodel.cpp
../RowLoader.cpp
../CipherDialog.cpp
../CipherDialog.ui
../CipherSettings.cpp
../Settings.cpp
../Data.cpp
../CondFormat.cpp
../sql/sqlitetypes.cpp
../sql/Query.cpp
../sql/ObjectIdentifier.cpp
../sql/parser/ParserDriver.cpp
../sql/parser/sqlite3_lexer.cpp
../sql/parser/sqlite3_parser.cpp
)

set(TESTRUNSQL_HDR
TestRunSql.h
../RunSql.h
../sqlitedb.h
)

add_executable(test-run-sql ${TESTRUNSQL_HDR} ${TESTRUNSQL_SRC})
target_link_libraries(test-run-sql ${QT_MAJOR}::Test ${QT_MAJOR}::Widgets ${QT_MAJOR}::Concurrent ${LIBSQLITE_NAME} ${QT5_COMPAT})
if(QT_MAJOR STREQUAL "Qt6")
set_target_properties(test-run-sql PROPERTIES AUTOUIC_OPTIONS "--connections=string")
endif()
add_test(NAME test-run-sql COMMAND $<TARGET_FILE:test-run-sql>)
set_tests_properties(test-run-sql PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
if(WIN32)
set(_rs_extra_paths "")
foreach(_p IN LISTS CMAKE_PREFIX_PATH)
list(APPEND _rs_extra_paths "${_p}" "${_p}/bin")
# OpenSSL 3 Universal installer layout: bin/x86, bin/x64
if(CMAKE_SIZEOF_VOID_P EQUAL 8)
list(APPEND _rs_extra_paths "${_p}/bin/x64")
else()
list(APPEND _rs_extra_paths "${_p}/bin/x86")
endif()
endforeach()
# SQLite and SQLCipher are built as DLLs in the CMake prefix by the Windows CI jobs.
string(REPLACE ";" "\\;" TESTRUNSQL_PATH "${_rs_extra_paths};$ENV{PATH}")
set_tests_properties(test-run-sql PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen;PATH=${TESTRUNSQL_PATH}")
endif()
140 changes: 140 additions & 0 deletions src/tests/TestRunSql.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
#include "TestRunSql.h"

#include "../RunSql.h"
#include "../sqlitedb.h"

#include <QApplication>
#include <QFileInfo>
#include <QTemporaryDir>
#include <QtTest/QtTest>

QTEST_MAIN(TestRunSql)

namespace
{

struct RunResult
{
QStringList errors;
int executed;
};

// Execute the complete script in the worker thread, just like the Execute SQL tab does, but without
// asking the user anything: the confirmation before committing is always answered with "Yes".
RunResult runScript(DBBrowserDB& db, const QString& sql)
{
RunResult result{{}, 0};

RunSql runner(db, sql, 0, sql.toUtf8().length(), false);
QObject::connect(&runner, &RunSql::statementErrored, &runner, [&result](const QString& message, int, int) {
result.errors << message;
}, Qt::DirectConnection);
QObject::connect(&runner, &RunSql::statementExecuted, &runner, [&result](const QString&, int, int) {
result.executed++;
}, Qt::DirectConnection);
QObject::connect(&runner, &RunSql::confirmSaveBeforePragmaOrVacuum, &runner, []() {
}, Qt::DirectConnection);

runner.start();
runner.wait();

return result;
}

struct Fixture
{
QTemporaryDir dir;
DBBrowserDB db;

Fixture()
{
if(!dir.isValid() || !db.create(dir.filePath("test.db")))
qFatal("Could not set up the test database");
if(!db.executeSQL("CREATE TABLE t(a INTEGER); INSERT INTO t VALUES(1);", false))
qFatal("Could not fill the test database");
}
~Fixture()
{
db.releaseAllSavepoints();
db.close();
}
};

}

void TestRunSql::queryTypeIgnoresLeadingComments_data()
{
QTest::addColumn<QString>("query");
QTest::addColumn<int>("type");

QTest::newRow("plain vacuum") << "VACUUM;" << int(RunSql::VacuumStatement);
QTest::newRow("line comment") << "-- compact\nVACUUM;" << int(RunSql::VacuumStatement);
QTest::newRow("block comment") << "/* compact */ vacuum" << int(RunSql::VacuumStatement);
QTest::newRow("several comments") << "-- a\n/* b */\n-- c\nVACUUM INTO 'x.db'" << int(RunSql::VacuumStatement);
QTest::newRow("pragma") << "-- set\nPRAGMA user_version = 2" << int(RunSql::PragmaStatement);
QTest::newRow("select") << "/* c */ SELECT 1" << int(RunSql::SelectStatement);
QTest::newRow("comment only mentioning vacuum") << "-- VACUUM\nSELECT 1" << int(RunSql::SelectStatement);
QTest::newRow("block comment mentioning vacuum") << "/* VACUUM */ UPDATE t SET a = 1" << int(RunSql::UpdateStatement);
}

void TestRunSql::queryTypeIgnoresLeadingComments()
{
QFETCH(QString, query);
QFETCH(int, type);

QCOMPARE(int(RunSql::getQueryType(query)), type);
}

void TestRunSql::vacuumPrecededByComment()
{
Fixture f;

// The database is clean here, so no savepoint may be created for the VACUUM. A leading comment must
// not hide the VACUUM from the statement type detection, otherwise SQLite refuses to run it inside
// the savepoint ("cannot VACUUM from within a transaction").
const RunResult result = runScript(f.db, "-- compact the database\nVACUUM;");

QVERIFY2(result.errors.isEmpty(), qPrintable(result.errors.join('\n')));
QCOMPARE(result.executed, 1);
QVERIFY(!f.db.getDirty());
}

void TestRunSql::vacuumFollowingModifyingStatementWithComment()
{
Fixture f;

const RunResult result = runScript(f.db,
"INSERT INTO t VALUES(2);\n"
"/* shrink the file */\n"
"VACUUM;\n"
"INSERT INTO t VALUES(3);");

QVERIFY2(result.errors.isEmpty(), qPrintable(result.errors.join('\n')));
QCOMPARE(result.executed, 3);
// The last INSERT is pending in a new savepoint, everything before it was committed by the VACUUM
QVERIFY(f.db.getDirty());
}

void TestRunSql::vacuumInto()
{
Fixture f;

const QString target = f.dir.filePath("copy.db");
const RunResult result = runScript(f.db, "-- backup\nVACUUM INTO '" + target + "';");

QVERIFY2(result.errors.isEmpty(), qPrintable(result.errors.join('\n')));
QVERIFY(QFileInfo::exists(target));
QVERIFY(!f.db.getDirty());
}

void TestRunSql::commentedVacuumIsNotVacuum()
{
Fixture f;

// A statement which merely mentions VACUUM in a comment must be executed like any other statement
// and be undoable, i.e. it must run inside a savepoint.
const RunResult result = runScript(f.db, "-- VACUUM\nINSERT INTO t VALUES(2);");

QVERIFY2(result.errors.isEmpty(), qPrintable(result.errors.join('\n')));
QVERIFY(f.db.getDirty());
}
19 changes: 19 additions & 0 deletions src/tests/TestRunSql.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#ifndef TESTRUNSQL_H
#define TESTRUNSQL_H

#include <QObject>

class TestRunSql : public QObject
{
Q_OBJECT

private slots:
void queryTypeIgnoresLeadingComments_data();
void queryTypeIgnoresLeadingComments();
void vacuumPrecededByComment();
void vacuumFollowingModifyingStatementWithComment();
void vacuumInto();
void commentedVacuumIsNotVacuum();
};

#endif // TESTRUNSQL_H
Loading