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
21 changes: 3 additions & 18 deletions src/MainWindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1217,26 +1217,11 @@ void MainWindow::executeQuery()
} break;
case Line:
{
// Start position is the first character of the current line, except for those cases where we're in the middle of a
// statement which started on one the previous line. In that case the start position is actually a bit earlier. For
// the end position we set the last character of the current line. If the statement(s) continue(s) into the next line,
// SQLite will execute it/them anyway and we'll stop afterwards.
// The positions are the ones of the line the cursor is in, or of the beginning of the statement if the line is in the middle of one
int execute_from_line, dummy;
editor->getCursorPosition(&execute_from_line, &dummy);
execute_from_position = editor->positionFromLineIndex(execute_from_line, 0);

// Need to set the end position here before adjusting the start line
int execute_to_line = execute_from_line;
int execute_to_index = editor->text(execute_to_line).remove('\n').remove('\r').length(); // This chops the line break at the end of the line
execute_to_position = editor->positionFromLineIndex(execute_to_line, execute_to_index);

QByteArray firstPartEntireSQL = sqlWidget->getSql().toUtf8().left(execute_from_position);
if(firstPartEntireSQL.lastIndexOf(';') != -1) {
execute_from_position -= firstPartEntireSQL.length() - firstPartEntireSQL.lastIndexOf(';') - 1;
} else {
// No semicolon before the current line, execute from the first line.
execute_from_position = editor->positionFromLineIndex(0, 0);
}
RunSql::getCurrentLineRange(sqlWidget->getSql().toUtf8(), editor->positionFromLineIndex(execute_from_line, 0),
execute_from_position, execute_to_position);

db.logSQL(tr("-- EXECUTING LINE IN '%1'\n--").arg(tabName), kLogMsg_User);
} break;
Expand Down
58 changes: 55 additions & 3 deletions src/RunSql.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,11 @@ RunSql::RunSql(DBBrowserDB& _db, QString query, int execute_from_position, int _
// Get lock to set up everything
std::unique_lock<std::mutex> lk(m);

// Cancel if there is nothing to execute
// Cancel if there is nothing to execute. Note that the positions are byte positions in the UTF-8 encoded query and not character positions,
// so we can't use them with QString::mid() because they differ as soon as there are multibyte characters in front of the part to execute.
const QString query_to_execute = QString::fromUtf8(query.toUtf8().mid(execute_from_position, execute_to_position-execute_from_position)).trimmed();
if(query.trimmed().isEmpty() || query.trimmed() == ";" || execute_from_position == execute_to_position ||
query.mid(execute_from_position, execute_to_position-execute_from_position).trimmed().isEmpty() ||
query.mid(execute_from_position, execute_to_position-execute_from_position).trimmed() == ";")
query_to_execute.isEmpty() || query_to_execute == ";")
return;

// All replacements in the query should be made by the same amount of characters, so the positions in the file
Expand Down Expand Up @@ -82,6 +83,15 @@ bool RunSql::executeNextStatement()
if(queries_left_to_execute.isEmpty())
return false;

// SQLite skips white space, comments and empty statements in front of a statement. So if there is nothing else left in the part we want to
// execute, the statement SQLite would find next belongs to a later part (e.g. the following line when executing the current line) and
// must not be executed.
if(execute_current_position + findStatementStart(queries_left_to_execute) >= execute_to_position)
{
stopExecution();
return false;
}

// Start execution timer
auto time_start = std::chrono::high_resolution_clock::now();

Expand Down Expand Up @@ -342,6 +352,48 @@ RunSql::StatementType RunSql::getQueryType(const QString& query)
return OtherStatement;
}

void RunSql::getCurrentLineRange(const QByteArray& sql, int line_start, int& execute_from_position, int& execute_to_position)
{
// The end position is the last character of the line. If the statement(s) continue(s) into the next line, SQLite will execute it/them anyway
// and we'll stop afterwards.
execute_to_position = line_start;
while(execute_to_position < sql.length() && sql.at(execute_to_position) != '\n' && sql.at(execute_to_position) != '\r')
execute_to_position++;

// The start position is the first character of the line, except for those cases where we're in the middle of a statement which started
// on one the previous lines. In that case the start position is actually a bit earlier.
const int last_semicolon = sql.left(line_start).lastIndexOf(';');
if(last_semicolon != -1)
execute_from_position = last_semicolon + 1;
else
execute_from_position = 0; // No semicolon before the current line, execute from the first line.
}

int RunSql::findStatementStart(const QByteArray& sql, int position)
{
const int length = sql.length();
while(position < length)
{
const char c = sql.at(position);
if(c == ' ' || c == '\t' || c == '\n' || c == '\f' || c == '\r' || c == ';')
{
position++;
} else if(c == '-' && position + 1 < length && sql.at(position + 1) == '-') {
// Comment until the end of the line
const int end = sql.indexOf('\n', position);
position = (end == -1) ? length : end + 1;
} else if(c == '/' && position + 1 < length && sql.at(position + 1) == '*') {
// Comment until the closing characters. It does not need to be closed.
const int end = sql.indexOf("*/", position + 2);
position = (end == -1) ? length : end + 2;
} else {
break;
}
}

return position;
}

void RunSql::acquireDbAccess()
{
pDb = db.get(tr("executing query"), true);
Expand Down
19 changes: 19 additions & 0 deletions src/RunSql.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,25 @@ class RunSql : public QThread

static StatementType getQueryType(const QString& query);

/**
* Determines the part of the SQL code to execute when only the line the cursor is in is to be executed.
* @param sql The complete SQL code, encoded as UTF-8
* @param line_start The position in bytes of the first character of the line
* @param execute_from_position Set to the position in bytes where the execution has to start. This is the first character of the line, except when the
* line is in the middle of a statement which started in one of the previous lines. Then it is the end of the statement before it.
* @param execute_to_position Set to the position in bytes of the end of the line, not including the line break. Statements which start in the line
* and continue in the following ones are executed completely. Nothing that starts after this position is.
*/
static void getCurrentLineRange(const QByteArray& sql, int line_start, int& execute_from_position, int& execute_to_position);

/**
* Skips everything which SQLite ignores in front of a statement: white space, comments and empty statements.
* @param sql The SQL code, encoded as UTF-8
* @param position The position in bytes where to start looking
* @return The position in bytes where the next statement actually starts or the length of the SQL code if there is none
*/
static int findStatementStart(const QByteArray& sql, int position = 0);

void startNextStatement();

void stop();
Expand Down
62 changes: 62 additions & 0 deletions src/tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,65 @@ 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
../CipherDialog.cpp
../CipherSettings.cpp
../CondFormat.cpp
../Data.cpp
../Settings.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
../CipherDialog.h
../CipherSettings.h
../CondFormat.h
../Data.h
../Settings.h
../sql/sqlitetypes.h
../sql/Query.h
../sql/ObjectIdentifier.h
../sql/parser/ParserDriver.h
../sql/parser/sqlite3_lexer.h
../sql/parser/sqlite3_location.h
../sql/parser/sqlite3_parser.hpp
)

add_executable(test-run-sql ${TESTRUNSQL_HDR} ${TESTRUNSQL_SRC} ../CipherDialog.ui)
target_include_directories(test-run-sql SYSTEM PRIVATE ${CMAKE_SOURCE_DIR}/libs/json)
if(QT_MAJOR STREQUAL "Qt6")
set_target_properties(test-run-sql PROPERTIES AUTOUIC_OPTIONS "--connections=string")
endif()
target_link_libraries(test-run-sql ${QT_MAJOR}::Test ${QT_MAJOR}::Widgets ${QT_MAJOR}::Concurrent ${LIBSQLITE_NAME} ${QT5_COMPAT})
add_test(NAME test-run-sql COMMAND $<TARGET_FILE:test-run-sql>)
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}")
else()
set_tests_properties(test-run-sql PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
endif()
Loading
Loading