mirror of
https://github.com/sqlitebrowser/sqlitebrowser.git
synced 2026-01-31 00:09:58 -06:00
Move SQL execution in Execute SQL tab into a separate thread
This commit is contained in:
@@ -30,6 +30,7 @@
|
||||
#include "CondFormat.h"
|
||||
#include "RunSql.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <QFile>
|
||||
#include <QApplication>
|
||||
#include <QTextStream>
|
||||
@@ -40,7 +41,6 @@
|
||||
#include <QDragEnterEvent>
|
||||
#include <QScrollBar>
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <QElapsedTimer>
|
||||
#include <QMimeData>
|
||||
#include <QColorDialog>
|
||||
#include <QDesktopServices>
|
||||
@@ -113,7 +113,8 @@ MainWindow::MainWindow(QWidget* parent)
|
||||
plotDock(new PlotDock(this)),
|
||||
remoteDock(new RemoteDock(this)),
|
||||
findReplaceDialog(new FindReplaceDialog(this)),
|
||||
gotoValidator(new QIntValidator(0, 0, this))
|
||||
gotoValidator(new QIntValidator(0, 0, this)),
|
||||
execute_sql_worker(nullptr)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
init();
|
||||
@@ -144,8 +145,8 @@ void MainWindow::init()
|
||||
#endif
|
||||
|
||||
// Connect SQL logging and database state setting to main window
|
||||
connect(&db, SIGNAL(dbChanged(bool)), this, SLOT(dbState(bool)));
|
||||
connect(&db, SIGNAL(sqlExecuted(QString, int)), this, SLOT(logSql(QString,int)));
|
||||
connect(&db, &DBBrowserDB::dbChanged, this, &MainWindow::dbState, Qt::QueuedConnection);
|
||||
connect(&db, &DBBrowserDB::sqlExecuted, this, &MainWindow::logSql, Qt::QueuedConnection);
|
||||
connect(&db, &DBBrowserDB::requestCollation, this, &MainWindow::requestCollation);
|
||||
|
||||
// Set the validator for the goto line edit
|
||||
@@ -162,11 +163,11 @@ void MainWindow::init()
|
||||
|
||||
// Set up DB structure tab
|
||||
dbStructureModel = new DbStructureModel(db, this);
|
||||
connect(&db, &DBBrowserDB::structureUpdated, [this]() {
|
||||
connect(&db, &DBBrowserDB::structureUpdated, this, [this]() {
|
||||
QString old_table = ui->comboBrowseTable->currentText();
|
||||
dbStructureModel->reloadData();
|
||||
populateStructure(old_table);
|
||||
});
|
||||
}, Qt::QueuedConnection);
|
||||
ui->dbTreeWidget->setModel(dbStructureModel);
|
||||
ui->dbTreeWidget->setColumnWidth(DbStructureModel::ColumnName, 300);
|
||||
ui->dbTreeWidget->setColumnHidden(DbStructureModel::ColumnObjectType, true);
|
||||
@@ -389,6 +390,11 @@ void MainWindow::init()
|
||||
plotDock->updatePlot(m_browseTableModel, &settings, true, false);
|
||||
});
|
||||
|
||||
connect(ui->actionSqlStop, &QAction::triggered, [this]() {
|
||||
if(execute_sql_worker && execute_sql_worker->isRunning())
|
||||
execute_sql_worker->stop();
|
||||
});
|
||||
|
||||
// Lambda function for keyboard shortcuts for selecting next/previous table in Browse Data tab
|
||||
connect(ui->dataTable, &ExtendedTableWidget::switchTable, [this](bool next) {
|
||||
int index = ui->comboBrowseTable->currentIndex();
|
||||
@@ -797,6 +803,19 @@ void MainWindow::applyBrowseTableSettings(BrowseDataTableSettings storedData, bo
|
||||
|
||||
bool MainWindow::fileClose()
|
||||
{
|
||||
// Stop any running SQL statements before closing the database
|
||||
if(execute_sql_worker && execute_sql_worker->isRunning())
|
||||
{
|
||||
if(QMessageBox::warning(this, qApp->applicationName(),
|
||||
tr("You are still executing SQL statements. When closing the database now the execution will be stopped. maybe "
|
||||
"leaving the database in an incosistent state. Are you sure you want to close the database?"),
|
||||
QMessageBox::Yes, QMessageBox::Cancel | QMessageBox::Default | QMessageBox::Escape) == QMessageBox::Cancel)
|
||||
return false;
|
||||
|
||||
execute_sql_worker->stop();
|
||||
execute_sql_worker->wait();
|
||||
}
|
||||
|
||||
// Close the database but stop the closing process here if the user pressed the cancel button in there
|
||||
if(!db.close())
|
||||
return false;
|
||||
@@ -1237,9 +1256,25 @@ void MainWindow::executeQuery()
|
||||
if(!db.isOpen())
|
||||
return;
|
||||
|
||||
// Check if other task is still running and stop it if necessary
|
||||
if(execute_sql_worker && execute_sql_worker->isRunning())
|
||||
{
|
||||
// Ask the user and do nothing if he/she doesn't want to interrupt the running query
|
||||
if(QMessageBox::warning(this, qApp->applicationName(),
|
||||
tr("You are already executing SQL statements. Do you want to stop them in order to execute the current "
|
||||
"statements instead? Note that this might leave the database in an inconsistent state."),
|
||||
QMessageBox::Yes, QMessageBox::Cancel | QMessageBox::Default | QMessageBox::Escape) == QMessageBox::Cancel)
|
||||
return;
|
||||
|
||||
// Stop the running query
|
||||
execute_sql_worker->stop();
|
||||
execute_sql_worker->wait();
|
||||
}
|
||||
|
||||
// Get current SQL tab and editor
|
||||
SqlExecutionArea* sqlWidget = qobject_cast<SqlExecutionArea*>(ui->tabSqlAreas->currentWidget());
|
||||
SqlTextEdit* editor = sqlWidget->getEditor();
|
||||
auto* current_tab = ui->tabSqlAreas->currentWidget();
|
||||
const QString tabName = ui->tabSqlAreas->tabText(ui->tabSqlAreas->currentIndex()).remove('&');
|
||||
|
||||
// Remove any error indicators
|
||||
@@ -1339,23 +1374,30 @@ void MainWindow::executeQuery()
|
||||
sqlWidget->finishExecution(log_message, ok);
|
||||
};
|
||||
|
||||
// Run the query
|
||||
RunSql r(db);
|
||||
connect(&r, &RunSql::statementErrored, [query_logger, this, sqlWidget](const QString& status_message, int from_position, int to_position) {
|
||||
// Prepare the SQL worker to run the query. We set the context of each signal-slot connection to the current SQL execution area.
|
||||
// This means that if the tab is closed all these signals are automatically disconnected so the lambdas won't be called for a not
|
||||
// existing execution area.
|
||||
execute_sql_worker.reset(new RunSql(db, sqlWidget->getSql(), execute_from_position, execute_to_position, true));
|
||||
|
||||
connect(execute_sql_worker.get(), &RunSql::statementErrored, sqlWidget, [query_logger, this, sqlWidget](const QString& status_message, int from_position, int to_position) {
|
||||
sqlWidget->getModel()->reset();
|
||||
ui->actionSqlResultsSave->setEnabled(false);
|
||||
ui->actionSqlResultsSaveAsView->setEnabled(false);
|
||||
attachPlot(sqlWidget->getTableResult(), sqlWidget->getModel());
|
||||
|
||||
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) {
|
||||
}, Qt::QueuedConnection);
|
||||
connect(execute_sql_worker.get(), &RunSql::statementExecuted, sqlWidget, [query_logger, this, sqlWidget](const QString& status_message, int from_position, int to_position) {
|
||||
sqlWidget->getModel()->reset();
|
||||
ui->actionSqlResultsSave->setEnabled(false);
|
||||
ui->actionSqlResultsSaveAsView->setEnabled(false);
|
||||
attachPlot(sqlWidget->getTableResult(), sqlWidget->getModel());
|
||||
|
||||
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();
|
||||
execute_sql_worker->startNextStatement();
|
||||
}, Qt::QueuedConnection);
|
||||
connect(execute_sql_worker.get(), &RunSql::statementReturnsRows, sqlWidget, [query_logger, this, sqlWidget](const QString& query, int from_position, int to_position, qint64 time_in_ms_so_far) {
|
||||
auto time_start = std::chrono::high_resolution_clock::now();
|
||||
|
||||
ui->actionSqlResultsSave->setEnabled(true);
|
||||
ui->actionSqlResultsSaveAsView->setEnabled(!db.readOnly());
|
||||
@@ -1363,20 +1405,62 @@ void MainWindow::executeQuery()
|
||||
auto * model = sqlWidget->getModel();
|
||||
model->setQuery(query);
|
||||
|
||||
// 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
|
||||
// Wait until the initial loading of data (= first chunk and row count) has been performed
|
||||
auto conn = std::make_shared<QMetaObject::Connection>();
|
||||
*conn = connect(model, &SqliteTableModel::finishedFetch, [=]() {
|
||||
// Disconnect this connection right now. This avoids calling this slot multiple times
|
||||
disconnect(*conn);
|
||||
|
||||
attachPlot(sqlWidget->getTableResult(), sqlWidget->getModel());
|
||||
connect(sqlWidget->getTableResult(), &ExtendedTableWidget::activated, this, &MainWindow::dataTableSelectionChanged);
|
||||
connect(sqlWidget->getTableResult(), &QTableView::doubleClicked, this, &MainWindow::doubleClickTable);
|
||||
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);
|
||||
auto time_end = std::chrono::high_resolution_clock::now();
|
||||
auto time_in_ms = std::chrono::duration_cast<std::chrono::milliseconds>(time_end-time_start);
|
||||
query_logger(true, tr("%1 rows returned in %2ms").arg(model->rowCount()).arg(time_in_ms.count()+time_in_ms_so_far), from_position, to_position);
|
||||
execute_sql_worker->startNextStatement();
|
||||
});
|
||||
}, Qt::QueuedConnection);
|
||||
connect(execute_sql_worker.get(), &RunSql::confirmSaveBeforePragmaOrVacuum, sqlWidget, [this]() {
|
||||
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::No)
|
||||
execute_sql_worker->stop();
|
||||
|
||||
}, Qt::BlockingQueuedConnection);
|
||||
connect(execute_sql_worker.get(), &RunSql::finished, sqlWidget, [this, current_tab, sqlWidget]() {
|
||||
// We work with a pointer to the current tab here instead of its index because the user might reorder the tabs in the meantime
|
||||
ui->tabSqlAreas->setTabIcon(ui->tabSqlAreas->indexOf(current_tab), QIcon());
|
||||
|
||||
// We don't need to check for the current SQL tab here because two concurrently running queries are not allowed
|
||||
ui->actionSqlExecuteLine->setEnabled(true);
|
||||
ui->actionExecuteSql->setEnabled(true);
|
||||
ui->actionSqlStop->setEnabled(false);
|
||||
sqlWidget->getEditor()->setReadOnly(false);
|
||||
|
||||
// Show Done message
|
||||
if(sqlWidget->inErrorState())
|
||||
sqlWidget->getStatusEdit()->setPlainText(tr("Execution finished with errors.") + "\n\n" + sqlWidget->getStatusEdit()->toPlainText());
|
||||
else
|
||||
sqlWidget->getStatusEdit()->setPlainText(tr("Execution finished without errors.") + "\n\n" + sqlWidget->getStatusEdit()->toPlainText());
|
||||
});
|
||||
|
||||
r.runStatements(sqlWidget->getSql(), execute_from_position, execute_to_position);
|
||||
// Add an hourglass icon to the current tab to indicate that there's a running execution in there.
|
||||
// NOTE It's a bit hack-ish but we don't use this icon just as a signal to the user but also check for it in various places to check whether a
|
||||
// specific SQL tab is currently running a query or not.
|
||||
ui->tabSqlAreas->setTabIcon(ui->tabSqlAreas->currentIndex(), QIcon(":icons/hourglass"));
|
||||
|
||||
// Deactivate the buttons to start a query and activate the button to stop the query
|
||||
ui->actionSqlExecuteLine->setEnabled(false);
|
||||
ui->actionExecuteSql->setEnabled(false);
|
||||
ui->actionSqlStop->setEnabled(true);
|
||||
|
||||
// Make the SQL editor widget read-only. We do this because the error indicators would be misplaced if the user changed the SQL text during execution
|
||||
sqlWidget->getEditor()->setReadOnly(true);
|
||||
|
||||
// Start the execution
|
||||
execute_sql_worker->start();
|
||||
}
|
||||
|
||||
void MainWindow::mainTabSelected(int tabindex)
|
||||
@@ -1976,6 +2060,20 @@ void MainWindow::closeSqlTab(int index, bool force)
|
||||
if(ui->tabSqlAreas->count() == 1 && !force)
|
||||
return;
|
||||
|
||||
// Check if we're still executing statements from this tab and stop them before proceeding
|
||||
if(!ui->tabSqlAreas->tabIcon(index).isNull())
|
||||
{
|
||||
if(QMessageBox::warning(this, qApp->applicationName(), tr("The statements in this tab are still executing. Closing the tab will stop the "
|
||||
"execution. This might leave the database in an inconsistent state. Are you sure "
|
||||
"you want to close the tab?"),
|
||||
QMessageBox::Yes,
|
||||
QMessageBox::Cancel | QMessageBox::Default | QMessageBox::Escape) == QMessageBox::Cancel)
|
||||
return;
|
||||
|
||||
execute_sql_worker->stop();
|
||||
execute_sql_worker->wait();
|
||||
}
|
||||
|
||||
// Remove the tab and delete the widget
|
||||
QWidget* w = ui->tabSqlAreas->widget(index);
|
||||
ui->tabSqlAreas->removeTab(index);
|
||||
@@ -2000,11 +2098,27 @@ unsigned int MainWindow::openSqlTab(bool resetCounter)
|
||||
return index;
|
||||
}
|
||||
|
||||
void MainWindow::changeSqlTab(int /*index*/)
|
||||
void MainWindow::changeSqlTab(int index)
|
||||
{
|
||||
// Instead of figuring out if there are some execution results in the new tab and which statement was used to generate them,
|
||||
// we just disable the export buttons in the toolbar.
|
||||
ui->actionSqlResultsSave->setEnabled(false);
|
||||
|
||||
// Check if the new tab is currently running a query or not
|
||||
if(ui->tabSqlAreas->tabIcon(index).isNull())
|
||||
{
|
||||
// Not running a query
|
||||
|
||||
ui->actionSqlExecuteLine->setEnabled(true);
|
||||
ui->actionExecuteSql->setEnabled(true);
|
||||
ui->actionSqlStop->setEnabled(false);
|
||||
} else {
|
||||
// Running a query
|
||||
|
||||
ui->actionSqlExecuteLine->setEnabled(false);
|
||||
ui->actionExecuteSql->setEnabled(false);
|
||||
ui->actionSqlStop->setEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::openSqlFile()
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
#include "Palette.h"
|
||||
#include "CondFormat.h"
|
||||
#include "sql/Query.h"
|
||||
#include "RunSql.h"
|
||||
|
||||
#include <memory>
|
||||
#include <QMainWindow>
|
||||
#include <QMap>
|
||||
|
||||
@@ -167,6 +169,8 @@ private:
|
||||
|
||||
Palette m_condFormatPalette;
|
||||
|
||||
std::unique_ptr<RunSql> execute_sql_worker;
|
||||
|
||||
void init();
|
||||
void clearCompleterModelsFields();
|
||||
|
||||
|
||||
@@ -941,6 +941,7 @@ You can drag SQL statements from an object row and drop them into other applicat
|
||||
<addaction name="separator"/>
|
||||
<addaction name="actionExecuteSql"/>
|
||||
<addaction name="actionSqlExecuteLine"/>
|
||||
<addaction name="actionSqlStop"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="actionSqlResultsSave"/>
|
||||
<addaction name="separator"/>
|
||||
@@ -2384,6 +2385,18 @@ You can drag SQL statements from the Schema column and drop them into the SQL ed
|
||||
<string>Ctrl+Shift+/</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionSqlStop">
|
||||
<property name="icon">
|
||||
<iconset resource="icons/icons.qrc">
|
||||
<normaloff>:/icons/cancel</normaloff>:/icons/cancel</iconset>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Stop SQL execution</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Stop the currently running SQL script</string>
|
||||
</property>
|
||||
</action>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
|
||||
135
src/RunSql.cpp
135
src/RunSql.cpp
@@ -3,23 +3,23 @@
|
||||
#include "sqlitedb.h"
|
||||
#include "sqlitetablemodel.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <QApplication>
|
||||
#include <QElapsedTimer>
|
||||
#include <QMessageBox>
|
||||
|
||||
RunSql::RunSql(DBBrowserDB& _db) :
|
||||
db(_db)
|
||||
RunSql::RunSql(DBBrowserDB& _db, QString query, int execute_from_position, int _execute_to_position, bool _interrupt_after_statements) :
|
||||
db(_db),
|
||||
may_continue_with_execution(true),
|
||||
interrupt_after_statements(_interrupt_after_statements),
|
||||
execute_current_position(execute_from_position),
|
||||
execute_to_position(_execute_to_position),
|
||||
structure_updated(false),
|
||||
savepoint_created(false),
|
||||
was_dirty(db.getDirty()),
|
||||
modified(false)
|
||||
{
|
||||
}
|
||||
|
||||
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();
|
||||
// Get lock to set up everything
|
||||
std::unique_lock<std::mutex> lk(m);
|
||||
|
||||
// Cancel if there is nothing to execute
|
||||
if(query.trimmed().isEmpty() || query.trimmed() == ";" || execute_from_position == execute_to_position ||
|
||||
@@ -37,23 +37,46 @@ void RunSql::runStatements(QString query, int execute_from_position, int _execut
|
||||
// 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);
|
||||
void RunSql::run()
|
||||
{
|
||||
// Execute statement by statement
|
||||
for(;;)
|
||||
{
|
||||
if(!executeNextStatement())
|
||||
break;
|
||||
}
|
||||
|
||||
// Execute each statement until there is nothing left to do
|
||||
while(executeNextStatement())
|
||||
;
|
||||
// Execution finished
|
||||
|
||||
// If the DB structure was changed by some command in this SQL script, update our schema representations
|
||||
if(structure_updated)
|
||||
db.updateSchema();
|
||||
}
|
||||
|
||||
QApplication::restoreOverrideCursor();
|
||||
void RunSql::startNextStatement()
|
||||
{
|
||||
std::unique_lock<std::mutex> lk(m);
|
||||
may_continue_with_execution = true;
|
||||
cv.notify_one();
|
||||
}
|
||||
|
||||
void RunSql::stop()
|
||||
{
|
||||
std::unique_lock<std::mutex> lk(m);
|
||||
|
||||
stopExecution();
|
||||
if(pDb)
|
||||
sqlite3_interrupt(pDb.get());
|
||||
may_continue_with_execution = true;
|
||||
cv.notify_all();
|
||||
}
|
||||
|
||||
bool RunSql::executeNextStatement()
|
||||
{
|
||||
std::unique_lock<std::mutex> lk(m);
|
||||
|
||||
// Is there anything left to do?
|
||||
if(queries_left_to_execute.isEmpty())
|
||||
return false;
|
||||
@@ -84,11 +107,11 @@ bool RunSql::executeNextStatement()
|
||||
// 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)
|
||||
lk.unlock();
|
||||
// Ask user, then check if we should abort execution or continue with it. We depend on a BlockingQueueConnection here which makes sure to
|
||||
// block this worker thread until the slot function in the main thread is completed and could tell us about its decision.
|
||||
emit confirmSaveBeforePragmaOrVacuum();
|
||||
if(!queries_left_to_execute.isEmpty())
|
||||
{
|
||||
// Commit all changes
|
||||
db.releaseAllSavepoints();
|
||||
@@ -97,6 +120,7 @@ bool RunSql::executeNextStatement()
|
||||
emit statementErrored(tr("Execution aborted by user"), execute_current_position, execute_current_position + (query_type == PragmaStatement ? 5 : 6));
|
||||
return false;
|
||||
}
|
||||
lk.lock();
|
||||
}
|
||||
} 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
|
||||
@@ -113,24 +137,25 @@ bool RunSql::executeNextStatement()
|
||||
|
||||
// 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();
|
||||
auto time_start = std::chrono::high_resolution_clock::now();
|
||||
|
||||
// Execute next statement
|
||||
const char* tail = queries_left_to_execute.data();
|
||||
int tail_length = queries_left_to_execute.length();
|
||||
lk.unlock();
|
||||
const char* qbegin = tail;
|
||||
auto pDb = db.get(tr("executing query"));
|
||||
acquireDbAccess();
|
||||
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
|
||||
lk.lock();
|
||||
queries_left_to_execute = QByteArray(tail);
|
||||
lk.unlock();
|
||||
|
||||
if (sql3status == SQLITE_OK)
|
||||
{
|
||||
@@ -152,9 +177,20 @@ bool RunSql::executeNextStatement()
|
||||
{
|
||||
// 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());
|
||||
releaseDbAccess();
|
||||
|
||||
lk.lock();
|
||||
may_continue_with_execution = false;
|
||||
|
||||
auto time_end = std::chrono::high_resolution_clock::now();
|
||||
auto time_in_ms = std::chrono::duration_cast<std::chrono::milliseconds>(time_end - time_start);
|
||||
emit statementReturnsRows(queryPart, execute_current_position, end_of_current_statement_position, time_in_ms.count());
|
||||
|
||||
// Make sure the next statement isn't executed until we're told to do so
|
||||
if(interrupt_after_statements)
|
||||
cv.wait(lk, [this](){ return may_continue_with_execution; });
|
||||
lk.unlock();
|
||||
break;
|
||||
}
|
||||
case SQLITE_DONE:
|
||||
@@ -167,29 +203,47 @@ bool RunSql::executeNextStatement()
|
||||
if(query_part_type == InsertStatement || query_part_type == UpdateStatement || query_part_type == DeleteStatement)
|
||||
stmtHasChangedDatabase = tr(", %1 rows affected").arg(sqlite3_changes(pDb.get()));
|
||||
|
||||
releaseDbAccess();
|
||||
|
||||
lk.lock();
|
||||
|
||||
// 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),
|
||||
may_continue_with_execution = false;
|
||||
|
||||
auto time_end = std::chrono::high_resolution_clock::now();
|
||||
auto time_in_ms = std::chrono::duration_cast<std::chrono::milliseconds>(time_end - time_start);
|
||||
emit statementExecuted(tr("query executed successfully. Took %1ms%2").arg(time_in_ms.count()).arg(stmtHasChangedDatabase),
|
||||
execute_current_position, end_of_current_statement_position);
|
||||
|
||||
// Make sure the next statement isn't executed until we're told to do so
|
||||
if(interrupt_after_statements)
|
||||
cv.wait(lk, [this](){ return may_continue_with_execution; });
|
||||
lk.unlock();
|
||||
break;
|
||||
}
|
||||
case SQLITE_MISUSE:
|
||||
break;
|
||||
default:
|
||||
emit statementErrored(QString::fromUtf8(sqlite3_errmsg(pDb.get())), execute_current_position, end_of_current_statement_position);
|
||||
QString error = QString::fromUtf8(sqlite3_errmsg(pDb.get()));
|
||||
releaseDbAccess();
|
||||
emit statementErrored(error, 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);
|
||||
QString error = QString::fromUtf8(sqlite3_errmsg(pDb.get()));
|
||||
releaseDbAccess();
|
||||
emit statementErrored(error, execute_current_position, end_of_current_statement_position);
|
||||
stopExecution();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Release the database
|
||||
pDb = nullptr;
|
||||
lk.lock();
|
||||
releaseDbAccess();
|
||||
|
||||
// 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:
|
||||
@@ -211,9 +265,6 @@ bool RunSql::executeNextStatement()
|
||||
return false;
|
||||
}
|
||||
|
||||
// Process events to keep the UI responsive
|
||||
qApp->processEvents();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -241,3 +292,13 @@ RunSql::StatementType RunSql::getQueryType(const QString& query)
|
||||
|
||||
return OtherStatement;
|
||||
}
|
||||
|
||||
void RunSql::acquireDbAccess()
|
||||
{
|
||||
pDb = db.get(tr("executing query"), true);
|
||||
}
|
||||
|
||||
void RunSql::releaseDbAccess()
|
||||
{
|
||||
pDb = nullptr;
|
||||
}
|
||||
|
||||
32
src/RunSql.h
32
src/RunSql.h
@@ -1,16 +1,22 @@
|
||||
#ifndef RUNSQL_H
|
||||
#define RUNSQL_H
|
||||
|
||||
#include <QObject>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <condition_variable>
|
||||
#include <QThread>
|
||||
|
||||
class DBBrowserDB;
|
||||
struct sqlite3;
|
||||
|
||||
class RunSql : public QObject
|
||||
class RunSql : public QThread
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
void run() override;
|
||||
|
||||
public:
|
||||
RunSql(DBBrowserDB& _db);
|
||||
RunSql(DBBrowserDB& db, QString query, int execute_from_position, int execute_to_position, bool interrupt_after_statements = false);
|
||||
~RunSql() override = default;
|
||||
|
||||
enum StatementType
|
||||
@@ -32,15 +38,29 @@ public:
|
||||
|
||||
static StatementType getQueryType(const QString& query);
|
||||
|
||||
void runStatements(QString _query, int execute_from_position, int _execute_to_position);
|
||||
void startNextStatement();
|
||||
|
||||
void stop();
|
||||
|
||||
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);
|
||||
|
||||
/**
|
||||
* This signal must be connected with a Qt::BlockingQueuedConnection in order to work as expected!
|
||||
*/
|
||||
void confirmSaveBeforePragmaOrVacuum();
|
||||
|
||||
private:
|
||||
DBBrowserDB& db;
|
||||
std::shared_ptr<sqlite3> pDb;
|
||||
|
||||
mutable std::mutex m;
|
||||
mutable std::condition_variable cv;
|
||||
bool may_continue_with_execution;
|
||||
|
||||
bool interrupt_after_statements;
|
||||
|
||||
QByteArray queries_left_to_execute;
|
||||
int execute_current_position;
|
||||
@@ -48,9 +68,13 @@ private:
|
||||
bool structure_updated;
|
||||
bool savepoint_created;
|
||||
bool was_dirty;
|
||||
bool modified;
|
||||
|
||||
void stopExecution();
|
||||
bool executeNextStatement();
|
||||
|
||||
void acquireDbAccess();
|
||||
void releaseDbAccess();
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
@@ -14,7 +14,8 @@ SqlExecutionArea::SqlExecutionArea(DBBrowserDB& _db, QWidget* parent) :
|
||||
QWidget(parent),
|
||||
db(_db),
|
||||
ui(new Ui::SqlExecutionArea),
|
||||
m_columnsResized(false)
|
||||
m_columnsResized(false),
|
||||
error_state(false)
|
||||
{
|
||||
// Create UI
|
||||
ui->setupUi(this);
|
||||
@@ -57,6 +58,7 @@ QString SqlExecutionArea::getSelectedSql() const
|
||||
|
||||
void SqlExecutionArea::finishExecution(const QString& result, const bool ok)
|
||||
{
|
||||
error_state = !ok;
|
||||
m_columnsResized = false;
|
||||
ui->editErrors->setPlainText(result);
|
||||
// Set reddish background when not ok
|
||||
@@ -97,6 +99,11 @@ ExtendedTableWidget *SqlExecutionArea::getTableResult()
|
||||
return ui->tableResult;
|
||||
}
|
||||
|
||||
QTextEdit* SqlExecutionArea::getStatusEdit()
|
||||
{
|
||||
return ui->editErrors;
|
||||
}
|
||||
|
||||
void SqlExecutionArea::saveAsCsv()
|
||||
{
|
||||
ExportDataDialog dialog(db, ExportDataDialog::ExportFormatCsv, this, model->query());
|
||||
|
||||
@@ -8,6 +8,8 @@ class SqliteTableModel;
|
||||
class DBBrowserDB;
|
||||
class ExtendedTableWidget;
|
||||
|
||||
class QTextEdit;
|
||||
|
||||
namespace Ui {
|
||||
class SqlExecutionArea;
|
||||
}
|
||||
@@ -29,6 +31,9 @@ public:
|
||||
SqliteTableModel* getModel() { return model; }
|
||||
SqlTextEdit* getEditor();
|
||||
ExtendedTableWidget *getTableResult();
|
||||
QTextEdit* getStatusEdit();
|
||||
|
||||
bool inErrorState() const { return error_state; }
|
||||
|
||||
public slots:
|
||||
void finishExecution(const QString& result, const bool ok);
|
||||
@@ -54,6 +59,7 @@ private:
|
||||
Ui::SqlExecutionArea* ui;
|
||||
bool m_columnsResized; // This is set to true if the columns of the table view were already adjusted to fit their contents
|
||||
bool showErrorIndicators;
|
||||
bool error_state;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
BIN
src/icons/hourglass.png
Normal file
BIN
src/icons/hourglass.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 744 B |
@@ -15,6 +15,7 @@
|
||||
#include <QDir>
|
||||
#include <QDateTime>
|
||||
#include <QDebug>
|
||||
#include <QThread>
|
||||
#include <functional>
|
||||
#include <atomic>
|
||||
#include <algorithm>
|
||||
@@ -599,12 +600,12 @@ bool DBBrowserDB::close()
|
||||
return true;
|
||||
}
|
||||
|
||||
DBBrowserDB::db_pointer_type DBBrowserDB::get(QString user)
|
||||
DBBrowserDB::db_pointer_type DBBrowserDB::get(QString user, bool force_wait)
|
||||
{
|
||||
if(!_db)
|
||||
return nullptr;
|
||||
|
||||
waitForDbRelease();
|
||||
waitForDbRelease(force_wait ? Wait : Ask);
|
||||
|
||||
db_user = user;
|
||||
db_used = true;
|
||||
@@ -618,6 +619,11 @@ void DBBrowserDB::waitForDbRelease(ChoiceOnUse choice)
|
||||
if(!_db)
|
||||
return;
|
||||
|
||||
// We can't show a message box from another thread than the main thread. So instead of crashing we
|
||||
// just decide that we don't interrupt any running query in this case.
|
||||
if(choice == Ask && QThread::currentThread() != QApplication::instance()->thread())
|
||||
choice = Wait;
|
||||
|
||||
std::unique_lock<std::mutex> lk(m);
|
||||
while(db_used) {
|
||||
// notify user, give him the opportunity to cancel that
|
||||
|
||||
@@ -82,10 +82,13 @@ public:
|
||||
\param user a string that identifies the new user, and which
|
||||
can be displayed in the dialog box.
|
||||
|
||||
\param force_wait if set to true we won't ask the user to cancel
|
||||
the running query but just wait until it is done.
|
||||
|
||||
\returns a unique_ptr containing the SQLite database handle, or
|
||||
nullptr in case no database is open.
|
||||
**/
|
||||
db_pointer_type get (QString user);
|
||||
db_pointer_type get (QString user, bool force_wait = false);
|
||||
|
||||
bool setSavepoint(const QString& pointname = "RESTOREPOINT");
|
||||
bool releaseSavepoint(const QString& pointname = "RESTOREPOINT");
|
||||
|
||||
@@ -91,6 +91,8 @@ void SqliteTableModel::handleRowCountComplete (int life_id, int num_rows)
|
||||
|
||||
m_rowCountAvailable = RowCount::Complete;
|
||||
handleFinishedFetch(life_id, num_rows, num_rows);
|
||||
|
||||
emit finishedRowCount();
|
||||
}
|
||||
|
||||
void SqliteTableModel::reset()
|
||||
|
||||
@@ -118,6 +118,7 @@ public slots:
|
||||
|
||||
signals:
|
||||
void finishedFetch(int fetched_row_begin, int fetched_row_end);
|
||||
void finishedRowCount();
|
||||
|
||||
protected:
|
||||
Qt::DropActions supportedDropActions() const override;
|
||||
|
||||
Reference in New Issue
Block a user