Move code for running SQL statements into a separate class

This is only done for better readability and for easing future changes.
No major logic changes intended here.
This commit is contained in:
Martin Kleusberg
2018-10-12 11:41:53 +02:00
parent 10b6c34be6
commit 7e0ff3061b
7 changed files with 365 additions and 268 deletions

View File

@@ -28,6 +28,7 @@
#include "FindReplaceDialog.h"
#include "Data.h"
#include "CondFormat.h"
#include "RunSql.h"
#include <QFile>
#include <QApplication>
@@ -1223,33 +1224,11 @@ void MainWindow::dataTableSelectionChanged(const QModelIndex& index)
}
}
MainWindow::StatementType MainWindow::getQueryType(const QString& query) const
{
// Helper function for getting the type of a given query
if(query.startsWith("SELECT", Qt::CaseInsensitive)) return SelectStatement;
if(query.startsWith("ALTER", Qt::CaseInsensitive)) return AlterStatement;
if(query.startsWith("DROP", Qt::CaseInsensitive)) return DropStatement;
if(query.startsWith("ROLLBACK", Qt::CaseInsensitive)) return RollbackStatement;
if(query.startsWith("PRAGMA", Qt::CaseInsensitive)) return PragmaStatement;
if(query.startsWith("VACUUM", Qt::CaseInsensitive)) return VacuumStatement;
if(query.startsWith("INSERT", Qt::CaseInsensitive)) return InsertStatement;
if(query.startsWith("UPDATE", Qt::CaseInsensitive)) return UpdateStatement;
if(query.startsWith("DELETE", Qt::CaseInsensitive)) return DeleteStatement;
if(query.startsWith("CREATE", Qt::CaseInsensitive)) return CreateStatement;
if(query.startsWith("ATTACH", Qt::CaseInsensitive)) return AttachStatement;
if(query.startsWith("DETACH", Qt::CaseInsensitive)) return DetachStatement;
return OtherStatement;
}
/*
* I'm still not happy how the results are represented to the user
* right now you only see the result of the last executed statement.
* A better experience would be tabs on the bottom with query results
* for all the executed statements.
* Or at least a some way the use could see results/status message
* per executed statement.
*/
void MainWindow::executeQuery()
{
@@ -1263,14 +1242,16 @@ void MainWindow::executeQuery()
SqlTextEdit* editor = sqlWidget->getEditor();
const QString tabName = ui->tabSqlAreas->tabText(ui->tabSqlAreas->currentIndex()).remove('&');
// Remove any error indicators
editor->clearErrorIndicators();
// Determine execution mode: execute all, execute selection or execute current line
enum executionMode
{
All,
Selection,
Line
};
executionMode mode;
} mode;
if(sender() && sender()->objectName() == "actionSqlExecuteLine")
mode = Line;
else if(!sqlWidget->getSelectedSql().isEmpty())
@@ -1279,22 +1260,19 @@ void MainWindow::executeQuery()
mode = All;
// Get SQL code to execute. This depends on the execution mode.
QString query = sqlWidget->getSql();
int execute_from_line = 0; // These three variables hold the start position
int execute_from_index = 0; // of the executed statements in the entire
int execute_from_position = 0; // SQL code of the current SQL tab.
int execute_to_line = 0; // These three variables hold the end position
int execute_to_index = 0; // of the executed statements in the entire
int execute_to_position = 0; // SQL code of the current SQL tab.
int execute_from_position = 0; // Where we want to start the execution in the query string
int execute_to_position = 0; // Where we roughly want to end the execution in the query string
switch(mode)
{
case Selection:
{
// Start and end positions are start and end positions from the selection
int execute_from_line, execute_from_index, execute_to_line, execute_to_index;
editor->getSelection(&execute_from_line, &execute_from_index, &execute_to_line, &execute_to_index);
execute_from_position = editor->positionFromLineIndex(execute_from_line, execute_from_index);
execute_to_position = editor->positionFromLineIndex(execute_to_line, execute_to_index);
db.logSQL(tr("-- EXECUTING SELECTION IN '%1'\n--").arg(tabName), kLogMsg_User);
} break;
case Line:
@@ -1303,21 +1281,19 @@ void MainWindow::executeQuery()
// 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.
int dummy;
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
execute_to_line = execute_from_line;
execute_to_index = editor->text(execute_to_line).length() - 1; // The -1 compensates for the line break at the end of the line
int execute_to_line = execute_from_line;
int execute_to_index = editor->text(execute_to_line).length() - 1; // The -1 compensates for the line break at the end of the line
execute_to_position = editor->positionFromLineIndex(execute_to_line, execute_to_index);
QByteArray firstPartEntireSQL = query.toUtf8().left(execute_from_position);
QByteArray firstPartEntireSQL = sqlWidget->getSql().toUtf8().left(execute_from_position);
if(firstPartEntireSQL.lastIndexOf(';') != -1)
{
execute_from_position -= firstPartEntireSQL.length() - firstPartEntireSQL.lastIndexOf(';') - 1;
editor->lineIndexFromPosition(execute_from_position, &execute_from_line, &execute_from_index);
}
db.logSQL(tr("-- EXECUTING LINE IN '%1'\n--").arg(tabName), kLogMsg_User);
} break;
case All:
@@ -1325,183 +1301,15 @@ void MainWindow::executeQuery()
// Start position is the first byte, end position the last.
// Note that we use byte positions that might differ from character positions.
execute_to_position = editor->length();
editor->lineIndexFromPosition(execute_to_position, &execute_to_line, &execute_to_index);
db.logSQL(tr("-- EXECUTING ALL IN '%1'\n--").arg(tabName), kLogMsg_User);
} break;
}
// Cancel if there is nothing to execute
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() == ";")
return;
// All replacements in the query should be made by the same amount of characters, so the positions in the file
// for error indicators and line and column logs are not displaced.
// Whitespace and comments are discarded by SQLite, so it is better to just let it ignore them.
query = query.replace(QRegExp("^(\\s*)BEGIN TRANSACTION;", Qt::CaseInsensitive), "\\1 ");
query = query.replace(QRegExp("COMMIT;(\\s*)$", Qt::CaseInsensitive), " \\1");
// Convert query to C string which we will use from now on, starting from the determined start position and
// until the end of the SQL code. By doing so we go further than the determined end position because in Line
// mode the last statement might go beyond that point.
QByteArray utf8Query = query.toUtf8().mid(execute_from_position);
// Remove any error indicators
editor->clearErrorIndicators();
// Set cursor and start execution timer
QApplication::setOverrideCursor(Qt::BusyCursor);
QElapsedTimer timer;
timer.start();
// Prepare execution
sqlite3_stmt* vm;
const char* tail = utf8Query.data();
int sql3status = SQLITE_OK;
int tail_length = utf8Query.length();
QString statusMessage;
bool ok = false;
bool modified = false;
const bool wasdirty = db.getDirty();
bool structure_updated = false;
bool savepoint_created = false;
// Accept multi-line queries, by looping until the tail is empty
while(tail && *tail != 0 && (sql3status == SQLITE_OK || sql3status == SQLITE_DONE))
{
// What type of query is this?
QString qtail = QString(tail).trimmed();
// Remove trailing comments so we don't get fooled by some trailing text at the end of the stream.
// Otherwise we'll pass them to SQLite and its execution will trigger a savepoint that wouldn't be
// reverted.
SqliteTableModel::removeCommentsFromQuery(qtail);
if (qtail.isEmpty())
break;
StatementType query_type = getQueryType(qtail);
// Check whether the DB structure is changed by this statement
if(!structure_updated && (query_type == AlterStatement ||
query_type == CreateStatement ||
query_type == DropStatement ||
query_type == RollbackStatement))
structure_updated = true;
// Check whether this is trying to set a pragma or to vacuum the database
if((query_type == PragmaStatement && qtail.contains('=') && !qtail.contains("defer_foreign_keys", Qt::CaseInsensitive)) || query_type == VacuumStatement)
{
// We're trying to set a pragma. If the database has been modified it needs to be committed first. We'll need to ask the
// user about that
if(db.getDirty())
{
if(QMessageBox::question(this,
QApplication::applicationName(),
tr("Setting PRAGMA values or vacuuming will commit your current transaction.\nAre you sure?"),
QMessageBox::Yes | QMessageBox::Default,
QMessageBox::No | QMessageBox::Escape) == QMessageBox::Yes)
{
// Commit all changes
db.releaseAllSavepoints();
} else {
// Abort
statusMessage = tr("Execution aborted by user");
break;
}
}
} else {
// We're not trying to set a pragma or to vacuum the database. In this case make sure a savepoint has been created in order to avoid committing
// all changes to the database immediately. Don't set more than one savepoint.
if(!savepoint_created)
{
// We have to start a transaction before we create the prepared statement otherwise every executed
// statement will get committed after the prepared statement gets finalized
db.setSavepoint();
savepoint_created = true;
}
}
// Execute next statement
int tail_length_before = tail_length;
const char* qbegin = tail;
auto pDb = db.get(tr("executing query"));
sql3status = sqlite3_prepare_v2(pDb.get(), tail, tail_length, &vm, &tail);
QString queryPart = QString::fromUtf8(qbegin, tail - qbegin);
tail_length -= (tail - qbegin);
int end_of_current_statement_position = execute_from_position + tail_length_before - tail_length;
if (sql3status == SQLITE_OK)
{
sql3status = sqlite3_step(vm);
sqlite3_finalize(vm);
// Get type
StatementType query_part_type = getQueryType(queryPart.trimmed());
// SQLite returns SQLITE_DONE when a valid SELECT statement was executed but returned no results. To run into the branch that updates
// the status message and the table view anyway manipulate the status value here. This is also done for PRAGMA statements as they (sometimes)
// return rows just like SELECT statements, too.
if((query_part_type == SelectStatement || query_part_type == PragmaStatement) && sql3status == SQLITE_DONE)
sql3status = SQLITE_ROW;
switch(sql3status)
{
case SQLITE_ROW:
{
// If we get here, the SQL statement returns some sort of data. So hand it over to the model for display. Don't set the modified flag
// because statements that display data don't change data as well.
pDb = nullptr;
auto * model = sqlWidget->getModel();
model->setQuery(queryPart);
// Wait until the initial loading of data (= first chunk and row count) has been performed. I have the
// feeling that a lot of stuff would need rewriting if we wanted to become more asynchronous here:
// essentially the entire loop over the commands would need to be signal-driven.
model->waitUntilIdle();
qApp->processEvents(); // to make row count available
statusMessage = tr("%1 rows returned in %2ms").arg(model->rowCount()).arg(timer.elapsed());
ok = true;
ui->actionSqlResultsSave->setEnabled(true);
ui->actionSqlResultsSaveAsView->setEnabled(!db.readOnly());
sql3status = SQLITE_OK;
break;
}
case SQLITE_DONE:
case SQLITE_OK:
{
// If we get here, the SQL statement doesn't return data and just executes. Don't run it again because it has already been executed.
// But do set the modified flag because statements that don't return data, often modify the database.
sqlWidget->getModel()->reset();
QString stmtHasChangedDatabase;
if(query_part_type == InsertStatement || query_part_type == UpdateStatement || query_part_type == DeleteStatement)
stmtHasChangedDatabase = tr(", %1 rows affected").arg(sqlite3_changes(pDb.get()));
// Attach/Detach statements don't modify the original database
if(query_part_type != StatementType::AttachStatement && query_part_type != StatementType::DetachStatement)
modified = true;
statusMessage = tr("query executed successfully. Took %1ms%2").arg(timer.elapsed()).arg(stmtHasChangedDatabase);
ok = true;
break;
}
case SQLITE_MISUSE:
continue;
default:
ok = false;
break;
}
timer.restart();
} else {
ok = false;
}
editor->lineIndexFromPosition(execute_from_position, &execute_from_line, &execute_from_index);
// Prepare a lambda function for logging the results of a query
auto query_logger = [this, sqlWidget, editor](bool ok, const QString& status_message, int from_position, int to_position) {
int execute_from_line, execute_from_index;
editor->lineIndexFromPosition(from_position, &execute_from_line, &execute_from_index);
// Special case: if the start position is at the end of a line, then move to the beggining of next line.
// Otherwise for the typical case, the line reference is one less than expected.
@@ -1511,13 +1319,11 @@ void MainWindow::executeQuery()
execute_from_index = 0;
}
// If there was an error, save the error message for later and highlight the erroneous SQL statement
if (!ok)
// If there was an error highlight the erroneous SQL statement
if(!ok)
{
statusMessage = QString::fromUtf8(sqlite3_errmsg(pDb.get()));
int end_of_current_statement_line, end_of_current_statement_index;
editor->lineIndexFromPosition(end_of_current_statement_position, &end_of_current_statement_line, &end_of_current_statement_index);
editor->lineIndexFromPosition(to_position, &end_of_current_statement_line, &end_of_current_statement_index);
editor->setErrorIndicator(execute_from_line, execute_from_index, end_of_current_statement_line, end_of_current_statement_index);
editor->setCursorPosition(execute_from_line, execute_from_index);
@@ -1525,44 +1331,52 @@ void MainWindow::executeQuery()
// Log the query and the result message.
// The query takes the last placeholder as it may itself contain the sequence '%' + number.
statusMessage = tr("-- At line %1:\n%4\n-- Result: %3").arg(execute_from_line+1).arg(statusMessage).arg(queryPart.trimmed());
db.logSQL(statusMessage, kLogMsg_User);
QString query = editor->text(from_position, to_position);
QString log_message = tr("-- At line %1:\n%3\n-- Result: %2").arg(execute_from_line+1).arg(status_message).arg(query.trimmed());
db.logSQL(log_message, kLogMsg_User);
// Release the database
pDb = nullptr;
// Update the execution area
sqlWidget->finishExecution(log_message, ok);
};
// Revert to save point now if it wasn't needed. We need to do this here because there are some rare cases where the next statement might
// be affected by what is only a temporary and unnecessary savepoint. For example in this case:
// ATTACH 'xxx' AS 'db2'
// SELECT * FROM db2.xy; -- Savepoint created here
// DETACH db2; -- Savepoint makes this statement fail
if(!modified && !wasdirty && savepoint_created)
{
db.revertToSavepoint(); // better rollback, if the logic is not enough we can tune it.
savepoint_created = false;
}
// Run the query
RunSql r(db);
connect(&r, &RunSql::statementErrored, [query_logger, this, sqlWidget](const QString& status_message, int from_position, int to_position) {
sqlWidget->getModel()->reset();
attachPlot(sqlWidget->getTableResult(), sqlWidget->getModel());
// Update the start position for the next statement and check if we are at
// the end of the part we want to execute. If so, stop the execution now.
execute_from_position = end_of_current_statement_position;
if(execute_from_position >= execute_to_position)
break;
query_logger(false, status_message, from_position, to_position);
});
connect(&r, &RunSql::statementExecuted, [query_logger, this, sqlWidget](const QString& status_message, int from_position, int to_position) {
sqlWidget->getModel()->reset();
attachPlot(sqlWidget->getTableResult(), sqlWidget->getModel());
// Process events to keep the UI responsive
qApp->processEvents();
}
query_logger(true, status_message, from_position, to_position);
});
connect(&r, &RunSql::statementReturnsRows, [query_logger, this, sqlWidget](const QString& query, int from_position, int to_position, qint64 time_in_ms) {
QElapsedTimer timer;
timer.start();
sqlWidget->finishExecution(statusMessage, ok);
attachPlot(sqlWidget->getTableResult(), sqlWidget->getModel());
ui->actionSqlResultsSave->setEnabled(true);
ui->actionSqlResultsSaveAsView->setEnabled(!db.readOnly());
connect(sqlWidget->getTableResult(), &ExtendedTableWidget::activated, this, &MainWindow::dataTableSelectionChanged);
connect(sqlWidget->getTableResult(), SIGNAL(doubleClicked(QModelIndex)), this, SLOT(doubleClickTable(QModelIndex)));
auto * model = sqlWidget->getModel();
model->setQuery(query);
// If the DB structure was changed by some command in this SQL script, update our schema representations
if(structure_updated)
db.updateSchema();
// Wait until the initial loading of data (= first chunk and row count) has been performed. I have the
// feeling that a lot of stuff would need rewriting if we wanted to become more asynchronous here:
// essentially the entire loop over the commands would need to be signal-driven.
model->waitUntilIdle();
qApp->processEvents(); // to make row count available
QApplication::restoreOverrideCursor();
attachPlot(sqlWidget->getTableResult(), sqlWidget->getModel());
connect(sqlWidget->getTableResult(), &ExtendedTableWidget::activated, this, &MainWindow::dataTableSelectionChanged);
connect(sqlWidget->getTableResult(), &QTableView::doubleClicked, this, &MainWindow::doubleClickTable);
query_logger(true, tr("%1 rows returned in %2ms").arg(model->rowCount()).arg(time_in_ms+timer.elapsed()), from_position, to_position);
});
r.runStatements(sqlWidget->getSql(), execute_from_position, execute_to_position);
}
void MainWindow::mainTabSelected(int tabindex)

View File

@@ -120,23 +120,6 @@ private:
int case_sensitive_like;
} pragmaValues;
enum StatementType
{
SelectStatement,
AlterStatement,
DropStatement,
RollbackStatement,
PragmaStatement,
VacuumStatement,
InsertStatement,
UpdateStatement,
DeleteStatement,
CreateStatement,
AttachStatement,
DetachStatement,
OtherStatement,
};
Ui::MainWindow* ui;
DBBrowserDB db;
@@ -199,8 +182,6 @@ private:
sqlb::ObjectIdentifier currentlyBrowsedTableName() const;
StatementType getQueryType(const QString& query) const;
void applyBrowseTableSettings(BrowseDataTableSettings storedData, bool skipFilters = false);
protected:

243
src/RunSql.cpp Normal file
View File

@@ -0,0 +1,243 @@
#include "RunSql.h"
#include "sqlite.h"
#include "sqlitedb.h"
#include "sqlitetablemodel.h"
#include <QApplication>
#include <QElapsedTimer>
#include <QMessageBox>
RunSql::RunSql(DBBrowserDB& _db) :
db(_db)
{
}
void RunSql::runStatements(QString query, int execute_from_position, int _execute_to_position)
{
// Save data for this task
execute_current_position = execute_from_position;
execute_to_position = _execute_to_position;
structure_updated = false;
savepoint_created = false;
was_dirty = db.getDirty();
// Cancel if there is nothing to execute
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() == ";")
return;
// All replacements in the query should be made by the same amount of characters, so the positions in the file
// for error indicators and line and column logs are not displaced.
// Whitespace and comments are discarded by SQLite, so it is better to just let it ignore them.
query = query.replace(QRegExp("^(\\s*)BEGIN TRANSACTION;", Qt::CaseInsensitive), "\\1 ");
query = query.replace(QRegExp("COMMIT;(\\s*)$", Qt::CaseInsensitive), " \\1");
// Convert query to byte array which we will use from now on, starting from the determined start position and
// until the end of the SQL code. By doing so we go further than the determined end position because in Line
// mode the last statement might go beyond that point.
queries_left_to_execute = query.toUtf8().mid(execute_from_position);
// Set cursor
QApplication::setOverrideCursor(Qt::BusyCursor);
// Execute each statement until there is nothing left to do
while(executeNextStatement())
;
// If the DB structure was changed by some command in this SQL script, update our schema representations
if(structure_updated)
db.updateSchema();
QApplication::restoreOverrideCursor();
}
bool RunSql::executeNextStatement()
{
// Is there anything left to do?
if(queries_left_to_execute.isEmpty())
return false;
// What type of query is this?
QString qtail = QString(queries_left_to_execute).trimmed();
// Remove trailing comments so we don't get fooled by some trailing text at the end of the stream.
// Otherwise we'll pass them to SQLite and its execution will trigger a savepoint that wouldn't be
// reverted.
SqliteTableModel::removeCommentsFromQuery(qtail);
if (qtail.isEmpty())
return false;
StatementType query_type = getQueryType(qtail);
// Check whether the DB structure is changed by this statement
if(!structure_updated && (query_type == AlterStatement ||
query_type == CreateStatement ||
query_type == DropStatement ||
query_type == RollbackStatement))
structure_updated = true;
// Check whether this is trying to set a pragma or to vacuum the database
// TODO This is wrong. The '=' or the 'defer_foreign_keys' might be in a completely different statement of the queries string.
if((query_type == PragmaStatement && qtail.contains('=') && !qtail.contains("defer_foreign_keys", Qt::CaseInsensitive)) || query_type == VacuumStatement)
{
// We're trying to set a pragma. If the database has been modified it needs to be committed first. We'll need to ask the
// user about that
if(db.getDirty())
{
if(QMessageBox::question(nullptr,
QApplication::applicationName(),
tr("Setting PRAGMA values or vacuuming will commit your current transaction.\nAre you sure?"),
QMessageBox::Yes | QMessageBox::Default,
QMessageBox::No | QMessageBox::Escape) == QMessageBox::Yes)
{
// Commit all changes
db.releaseAllSavepoints();
} else {
// Abort
emit statementErrored(tr("Execution aborted by user"), execute_current_position, execute_current_position + (query_type == PragmaStatement ? 5 : 6));
return false;
}
}
} else {
// We're not trying to set a pragma or to vacuum the database. In this case make sure a savepoint has been created in order to avoid committing
// all changes to the database immediately. Don't set more than one savepoint.
if(!savepoint_created)
{
// We have to start a transaction before we create the prepared statement otherwise every executed
// statement will get committed after the prepared statement gets finalized
db.setSavepoint();
savepoint_created = true;
}
}
// Start execution timer. We do that after opening any message boxes and after creating savepoints because both are not part of the actual
// query execution.
QElapsedTimer timer;
timer.start();
// Execute next statement
const char* tail = queries_left_to_execute.data();
int tail_length = queries_left_to_execute.length();
const char* qbegin = tail;
auto pDb = db.get(tr("executing query"));
sqlite3_stmt* vm;
int sql3status = sqlite3_prepare_v2(pDb.get(), tail, tail_length, &vm, &tail);
QString queryPart = QString::fromUtf8(qbegin, tail - qbegin);
int tail_length_before = tail_length;
tail_length -= (tail - qbegin);
int end_of_current_statement_position = execute_current_position + tail_length_before - tail_length;
bool modified = false;
// Save remaining statements
queries_left_to_execute = QByteArray(tail);
if (sql3status == SQLITE_OK)
{
sql3status = sqlite3_step(vm);
sqlite3_finalize(vm);
// Get type
StatementType query_part_type = getQueryType(queryPart.trimmed());
// SQLite returns SQLITE_DONE when a valid SELECT statement was executed but returned no results. To run into the branch that updates
// the status message and the table view anyway manipulate the status value here. This is also done for PRAGMA statements as they (sometimes)
// return rows just like SELECT statements, too.
if((query_part_type == SelectStatement || query_part_type == PragmaStatement) && sql3status == SQLITE_DONE)
sql3status = SQLITE_ROW;
switch(sql3status)
{
case SQLITE_ROW:
{
// If we get here, the SQL statement returns some sort of data. So hand it over to the model for display. Don't set the modified flag
// because statements that display data don't change data as well.
pDb = nullptr;
emit statementReturnsRows(queryPart, execute_current_position, end_of_current_statement_position, timer.elapsed());
break;
}
case SQLITE_DONE:
case SQLITE_OK:
{
// If we get here, the SQL statement doesn't return data and just executes. Don't run it again because it has already been executed.
// But do set the modified flag because statements that don't return data, often modify the database.
QString stmtHasChangedDatabase;
if(query_part_type == InsertStatement || query_part_type == UpdateStatement || query_part_type == DeleteStatement)
stmtHasChangedDatabase = tr(", %1 rows affected").arg(sqlite3_changes(pDb.get()));
// Attach/Detach statements don't modify the original database
if(query_part_type != StatementType::AttachStatement && query_part_type != StatementType::DetachStatement)
modified = true;
emit statementExecuted(tr("query executed successfully. Took %1ms%2").arg(timer.elapsed()).arg(stmtHasChangedDatabase),
execute_current_position, end_of_current_statement_position);
break;
}
case SQLITE_MISUSE:
break;
default:
emit statementErrored(QString::fromUtf8(sqlite3_errmsg(pDb.get())), execute_current_position, end_of_current_statement_position);
stopExecution();
return false;
}
} else {
emit statementErrored(QString::fromUtf8(sqlite3_errmsg(pDb.get())), execute_current_position, end_of_current_statement_position);
stopExecution();
return false;
}
// Release the database
pDb = nullptr;
// Revert to save point now if it wasn't needed. We need to do this here because there are some rare cases where the next statement might
// be affected by what is only a temporary and unnecessary savepoint. For example in this case:
// ATTACH 'xxx' AS 'db2'
// SELECT * FROM db2.xy; -- Savepoint created here
// DETACH db2; -- Savepoint makes this statement fail
if(!modified && !was_dirty && savepoint_created)
{
db.revertToSavepoint();
savepoint_created = false;
}
// Update the start position for the next statement and check if we are at
// the end of the part we want to execute. If so, stop the execution now.
execute_current_position = end_of_current_statement_position;
if(execute_current_position >= execute_to_position)
{
stopExecution();
return false;
}
// Process events to keep the UI responsive
qApp->processEvents();
return true;
}
void RunSql::stopExecution()
{
queries_left_to_execute.clear();
}
RunSql::StatementType RunSql::getQueryType(const QString& query)
{
// Helper function for getting the type of a given query
if(query.startsWith("SELECT", Qt::CaseInsensitive)) return SelectStatement;
if(query.startsWith("ALTER", Qt::CaseInsensitive)) return AlterStatement;
if(query.startsWith("DROP", Qt::CaseInsensitive)) return DropStatement;
if(query.startsWith("ROLLBACK", Qt::CaseInsensitive)) return RollbackStatement;
if(query.startsWith("PRAGMA", Qt::CaseInsensitive)) return PragmaStatement;
if(query.startsWith("VACUUM", Qt::CaseInsensitive)) return VacuumStatement;
if(query.startsWith("INSERT", Qt::CaseInsensitive)) return InsertStatement;
if(query.startsWith("UPDATE", Qt::CaseInsensitive)) return UpdateStatement;
if(query.startsWith("DELETE", Qt::CaseInsensitive)) return DeleteStatement;
if(query.startsWith("CREATE", Qt::CaseInsensitive)) return CreateStatement;
if(query.startsWith("ATTACH", Qt::CaseInsensitive)) return AttachStatement;
if(query.startsWith("DETACH", Qt::CaseInsensitive)) return DetachStatement;
return OtherStatement;
}

56
src/RunSql.h Normal file
View File

@@ -0,0 +1,56 @@
#ifndef RUNSQL_H
#define RUNSQL_H
#include <QObject>
class DBBrowserDB;
class RunSql : public QObject
{
Q_OBJECT
public:
RunSql(DBBrowserDB& _db);
~RunSql() override = default;
enum StatementType
{
SelectStatement,
AlterStatement,
DropStatement,
RollbackStatement,
PragmaStatement,
VacuumStatement,
InsertStatement,
UpdateStatement,
DeleteStatement,
CreateStatement,
AttachStatement,
DetachStatement,
OtherStatement,
};
static StatementType getQueryType(const QString& query);
void runStatements(QString _query, int execute_from_position, int _execute_to_position);
signals:
void statementErrored(QString message, int from_position, int to_position);
void statementExecuted(QString message, int from_position, int to_position);
void statementReturnsRows(QString query, int from_position, int to_position, qint64 time_in_ms);
private:
DBBrowserDB& db;
QByteArray queries_left_to_execute;
int execute_current_position;
int execute_to_position;
bool structure_updated;
bool savepoint_created;
bool was_dirty;
void stopExecution();
bool executeNextStatement();
};
#endif

View File

@@ -4,7 +4,6 @@
#include <QAbstractTableModel>
#include <QStringList>
#include <QVector>
#include <QThread>
#include <QMutex>
#include <QColor>
#include <memory>

View File

@@ -71,7 +71,8 @@ HEADERS += \
DotenvFormat.h \
Palette.h \
CondFormat.h \
sql/Query.h
sql/Query.h \
RunSql.h
SOURCES += \
sqlitedb.cpp \
@@ -119,7 +120,8 @@ SOURCES += \
DotenvFormat.cpp \
Palette.cpp \
CondFormat.cpp \
sql/Query.cpp
sql/Query.cpp \
RunSql.cpp
RESOURCES += icons/icons.qrc \
translations/flags/flags.qrc \